Gmaps with Rails 6/Webpack - ruby-on-rails

I'm trying to get my Google Maps setup that previously worked with earlier versions of Rails to display using Rails 6. Obviously Rails 6 is now using webpack to handle javascript assets and I can't get my app to recognie the Gmaps function used to render the map.
Some of the basics:
Gemfile
gem 'geocoder'
gem 'gmaps4rails'
gem 'underscore-rails'
# maybe don't need ^ this underscore gem anymore
I installed underscore with yarn add underscore and also added the google maps script gmaps_google.js currently under vendor/javascripts folder which has been added to my resolved paths in webpacker.yml `app/javascript/packs/application.js file looks like:
require("#rails/ujs").start()
require("#rails/activestorage").start()
require("channels")
require("gmaps_google")
import "bootstrap";
import 'underscore'
import "../stylesheets/application";
window.jQuery = $;
window.$ = $;
Here is my actual show.html.erb view and attempted load of the map
<div style='width: 800px;'>
<div id="map" style='width: 800px; height: 400px;'></div>
</div>
<script>
const handler = Gmaps.build('Google');
handler.buildMap({ provider: {}, internal: {id: 'map'}}, function(){
markers = handler.addMarkers([
{
"lat": <%= #user.latitude %>,
"lng": <%= #user.longitude %>,
"infowindow": "<%= #user.full_name %>'s location"
}
]);
handler.bounds.extendWith(markers);
handler.fitMapToBounds();
handler.getMap().setZoom(17);
});
</script>
Yet I still get no map loading on the page an error that says
Uncaught ReferenceError: Gmaps is not defined
It does load, and the error goes away when I load the gmaps script from an external CDN source in my HTML head, as this question suggests: Gmaps is not defined
So it is clearly just the loading/availability of that google_maps.js script. It's too massive of a file to show here, but here is the link to the working CDN version: cdn.jsdelivr.net/gmaps4rails/2.1.2/gmaps4rails.js
Copy-pasting that into my google_maps.js file doesn't help though. And I'm trying to figure out how I can get it to work with the Google Maps script residing within my Rails application and the webpack Rails 6 world is still very new to me. Any suggestions would be greatly appreciated.

The quick-and-dirty way is just load the gmaps4rails code (and underscore) from CDN instead of Webpack. Since you're just using a <script> tag to build the map, Webpack is not helping you there.
If you'd like to try using Webpack anyways, read on.
Understand that Webpack isolates itself from the global scope; unlike Sprockets, objects or values you import in Webpack will not be available in <script> tags or the browser console. If you want this behavior, you have to configure it by hand.
Looking at the gmaps4rails script, it appears not to be module-aware and assumes it's evaluated in the global scope (where underscore is available). In other words, it's not module-friendly. But we can make it play nice.
We need to tell Webpack to treat this in the gmaps4rails script as window, i.e., the global scope, and to "provide" underscore (or nearly interchangeable lodash, as below) since it assumes the _ lib is available in its scope.
In your shell:
$ yarn add imports-loader lodash
In config/webpack/environment.js:
const { environment } = require('#rails/webpacker')
const webpack = require('webpack')
environment.loaders.append('gmap4rails', {
test: /gmaps_google/,
use: [
{
loader: 'imports-loader',
options: 'this=>window',
},
],
})
environment.plugins.append(
'lodash',
new webpack.ProvidePlugin({
_: 'lodash',
})
)
module.exports = environment
Assuming you have set up your google scripts correctly per the gmaps4rails README, this should do the trick.
I've created a demo app with a working example in the example/gmap4rails branch (bring your own Google Maps API key): https://github.com/rossta/rails6-webpacker-demo/tree/example/gmaps4rails

Related

How to call a javascript function inside a rails view?

I did just a upgrade from RAILS 5 to RAILS 6 and I see that all rails views are not able to call a javascript function as before in RAILS 5.
I have an external javascript file located under
app/javascript/packs/station.js
This is is embeded in in app/views/layouts/application.html.erb as
<%= javascript_pack_tag 'station' %>
This is the code how I call the javascrpt function from html.erb file :
<%= text_field_tag(:station_text_field, ... ,
onkeyup: "javascript: request_stations(); ") %>
When I try to call a function thats is part of the station.js then I get an error in the browser developmer view: ReferenceError: request_stations is not defined
But I can also see in the brwoser view, under Debugger :
Webpack / app/javascript / packs / station.js
and the javascript function I want to call.
So it seems that this script was loaded by the browser.
In contrast, when I just copy and paste these few lines that represent this javascript function direct into the template view file (...html.erb), something like :
<script>
function request_stations ()
{
alert("calling request_stations");
};
</script>
then - it works as expected !
By default, variables/functions defined inside JavaScript files that are packed by Webpacker will not be available globally.
This is a good thing, because it prevents global naming conflicts. Generally speaking, you don't want to reference javascript functions/variables from your view. You instead want to write JavaScript in a way that attaches functionality to DOM nodes using their id or other attributes.
Here is a basic example based on the code you provided:
# in your rails view
<%= text_field_tag(:station_text_field, ..., id: 'station-text-field') %>
// in your javascript
function request_stations() {
alert("calling request_stations");
};
const stationTextField = document.querySelector("#station-text-field");
stationTextField.addEventListener('keyup', (event) => {
request_stations();
});
Agree with mhunter's answer.
This post helped me get a grounding on this difference in Rails 6: https://blog.capsens.eu/how-to-write-javascript-in-rails-6-webpacker-yarn-and-sprockets-cdf990387463
What I don't see in your question is whether or not you did this in app/javascript/packs/application.js:
require("#rails/ujs").start()
require("turbolinks").start()
require("#rails/activestorage").start()
require("channels")
require("station")
The big difference in Rails 6 is that you have to deliberately:
require a JS file
deliberately export something from that file
deliberately import that something, in the file where you want to use it.
So if there is a function in station.js that you want to use, connect the steps above. Start with a simple function in station.js that fires upon DOMContentLoaded, and add a console.log("hey, station.js is alive and well"). If you don't see it, then something in those 3 steps is not right.
In pre-Rails6, you had a "garden" of JavaScript, just by virtue of being in the asset pipeline. In Rails 6, you have to be more deliberate.

Chart is not defined in a Rails 6 view page with webpacker

I'm trying to figure out the right way to use a Chart from chart.js in a Rails 6 app using webpacker. I would like my chart generation javascript code to remain in the page since I'll be dynamically generating it from the view.
I started with
yum add chartjs
then added to the application pack (and javascript_pack_tag is in my application.html.erb)
// app/javascript/packs/application.js
import Chart from "chartjs";
And then, in a view I try and use that library:
<div class="row">
<div class="col">
<canvas id="backlog-chart" width="100%" height="400px">
<script>
var ctx = document.getElementById('backlog-chart').getContext('2d');
new Chart(ctx, { ...
This fails with an Chart is not defined javascript error.
Things I've tried:
the require("chartjs") form of including the library in application.js seems to have no impact one way or the other.
I tried adding to config/webpack/environment.js to the ProvidePlugin object with Chart: "chartjs/chart.js" but no changes.
And I've tried adding import Chart from "chart.js" directly to the <script> section in the view but this fails with a "import only allowed in module" error on the browser.
My javascript skills have definitely not kept up with the last couple years of new technologies so any suggestions on the right way to do this would be appreciated.
You can't use Chart in a script in your HTML body because Chart hasn't been globably defined. You could import the chart.js javascript by embedding the script tag, and then Chart would be available. But you said you wanted to use Webpacker, so...
Include your chart definition in your application.js, rather than in the script tag.
// app/javascript/packs/application.js
import Chart from "chartjs";
var ctx = document.getElementById('backlog-chart').getContext('2d');
var myChart = new Chart(ctx, {...});
This will target that canvas in your HTML:
<canvas id="backlog-chart" ... />

Vue.js app works in development but not mounting template in production with Rails 5.2.0 / Webpacker - blank screen with no errors in console

I'm using Rails 5.2.0 and Webpacker gem to deploy a Vue application.
The show.html.erb file is very simple:
<div data-behavior="vue-app"><MyComponent></MyComponent></div>
And then in my entry pack, packs/my_vue_app.js:
import TurbolinksAdapter from 'vue-turbolinks';
import Vue from 'vue/dist/vue.esm'
Vue.use(TurbolinksAdapter);
import MyComponent from '../components/my_app/index.vue'
document.addEventListener('turbolinks:load', () => {
var element = $('[data-behavior="vue-app"]');
if(!element) { return };
console.log('Initializing Vue');
const app = new Vue({
el: element[0],
data: {
},
components: { MyComponent }
})
})
In development, everything works absolutely fine. The app is mounted and functional.
But in production, after the page load and JS runs, <div data-behavior="vue-app"> is removed from the paging, leaving only <!-- --> in it's place.
In the console, there are absolutely no errors. I can confirm using DevTools that the pack js file is loaded, and it was parsed, since the console.log is printed in the console.
Heck, the proof that Vue is working is that the entire <div> where it was mounted was removed from DOM after JS parsing.
The weirdest thing of all is that I could get the app to mount ONCE, by attaching a debugger on the console.log line and turning it off while the debugger paused execution. Even tough I saw the app mounting that time, I could not get it to mount later on, even fiddling with the debugger again ... it's really, really weird.
These are the versions of package.json:
"vue": "^2.5.16",
"vue-loader": "14.2.2",
"vue-template-compiler": "^2.5.16",
The Rails app is brand new, with no config other than the default.
Webpacker gem is 3.5.3 and Rails is 5.2.0.
After spending a really long time on this, I only found this github issue: https://github.com/rails/webpacker/issues/1520
EDIT: I'm providing a link to the real, production app where this bug is happening: https://planilha.tramitacaointeligente.com.br/planilhas/ED2sUXz32-R9CJKdkmtf8Q
You'll see it's not mounting. Here's the same page in development:
I eventually managed to solve it by changing how the Vue app loading was defined.
Try import Vue from 'vue' (instead of from 'vue/dist/vue.esm') and then:
const app = new Vue({
el: domElement,
render: h => h(RootComponent)
})
The comments that appear in the hello_vue.js scaffold from the Webpacker gem tell you that you can choose between using the DOM as your template OR load the component with a render function; they both do work in development, but only the latter (loading the component with a render function, using vue instead of vue/dist/vue.esm and render: h => h(RootComponent) worked for me in production.
This has been, by far, the longest, most frustrating debugging session of my life, since there are absolutely no errors in console, you just stare into a blank screen, and Vue is running since it removes the DOM element it was mounted to from the DOM.
Source of solution: https://stackoverflow.com/a/48651338/1290457 and here's the github issue (currently open) on Webpacker gem https://github.com/rails/webpacker/issues/1520
I still don't know how to use DOM as template with Vue in production tough.
I had similar problem in Rails 5.2 + webpack Vue. All was good in development, but not working in production. After hours of investigating I found the reason. It was in this recommendation from webpaker gem docs.
Adding this
Rails.application.config.content_security_policy do |policy|
if Rails.env.development?
policy.script_src :self, :https, :unsafe_eval
else
policy.script_src :self, :https
end
end
brokes production. Removing else part - fixing the situation.
Chrome silently ignoring this. Firefox shows warnings.

How to migrate requiring of select2.js from sprockets to webpacker

I'm in the process of migrating a Rails 5.1.5 project, which uses CoffeeScript, from using sprockets to using webpacker. The project also uses select2.js. With sprockets, I did the following:
Install jquery-rails (jQuery is a dependency for select2).
Put select2.js code in vendor/assets/javscripts.
In application.js.coffee, add:
#= require select2
After that I was able to use select2 to in my application.js.coffee file:
$(document).on 'turbolinks:load' ->
$('select').select2
So far I've described the pretty standard way of including/using javascript libraries with sprockets.
However, with webpacker I can't make select2 work and I'm not sure why. I have two hypothesis:
I'm not importing/requiring it properly;
it doesn't find jQuery at some point of the load process;
So for jQuery, I did the following:
yarn add jquery
included in my environment.js:
const webpack = require('webpack');
environment.plugins.append('Provide', new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}));
I've removed the jquery-rails gem, as well as #= require 'jquery' and tested that jquery works, so I guess I have correctly included it. However, I tried several ways of importing select2 (using es6 imports) and none of them worked. I tried:
import select2 from 'select2';
import select2 from 'select2/dist/js/select2'
import select2 from 'select2/dist/js/select2.js'
import 'select2/dist/js/select2.js'
I even tried to import it from the old vendor location by writing inside app/javascript/pack/application.js.coffee:
import '../../../vendor/assets/javascripts/select2'
I can confirm that the file contents is imported, as I put a console.log within the select2 file under node_modules/select2/dist/js/select.js and it did get printed. However, I also get the error TypeError: $(...).select2 is not a function when I execute $('select').select2() in the browser's dev tool console.
What am I missing or doing wrong?
P.S. I can provide much more info, but I didn't want my question to get any more bloated.
P.P.S. With my modest JS knowledge, I looked at the source code but couldn't recognize what exactly they are exporting and how am I supposed to import it.
I know this is an old post, but just in case someone else could benefit:
app/javascript/packs/application.js
...other requires...
require('select2')
window.Rails = Rails
import 'bootstrap'
...other imports...
import 'select2'
import 'select2/dist/css/select2.css'
$(document).on("turbolinks:load", () => {
$('.select2').select2()
})
My similar problem
I have stumble upon the same problem with another web component (Switchery):
I imported the component with yarn add switchery (no error)
I could import it correctly through WebPack with import 'switchery' (no error bundling the pack)
But when I was trying to use the Switchery object in the browser like they say in the doc:
var elem = document.querySelector('.js-switch');
var init = new Switchery(elem);
I would get the error: ReferenceError: Switchery is not defined
Note: I didn't want to install RequireJS as WebPack is supposed to do the same thing (and even better) nowadays.
My solution:
The problem was the webpack doesn't expose the pack-generated variables and classes in the global scope!
So to fix this, I needed to do two things:
Explicitly give a name to the imported class from Switchery:
import Switchery from 'switchery'
Use this Class only in the same JS file where the import was done
Testing hack:
If you want to try that out and "go back" to the mess that sprocket allowed, in the same file, you can expose "globally" the variable so you can use in from the browser:
import Switchery from 'switchery'
window.Switchery = Swicthery
now you can execute the switchery almost like in the example:
var init = new window.Switchery(elem);
Hope that helps...

Google Maps API not working in Rails once Coffeescript is compiled for Production

Running into a bit of a headache with some Coffeescript and/or Rails behaviour. Everything works fine when run in development (non-compiled JS) but once I pushed to production I started getting:
Uncaught InvalidValueError: initMap is not a function
Here's the coffeescript I am using (stripped down to basics):
jQuery ->
#map = null
new googleMap()
class googleMap
window.initMap = ->
#map = new (google.maps.Map)(document.getElementById('map-overlay'))
And it is being called as per the Google Maps API V3 documentation with the follow script loaded at the bottom just below the </body> tag on my page.
<script async="async" defer="defer" src="https://maps.googleapis.com/maps/api/js?key=*snip*&callback=initMap"></script>
I'm guessing it has something to do with the way the JS is compiled and gets wrapped in an unnamed function but I've spent a bit of time trying to figure it out and am not getting any further.
Any help would be appreciated.
A nice trick for using google maps asynconously without globals is to use jQuery.deferred together with the Google API loader.
apiLoaded = jQuery.Deferred()
mapInit = jQuery.Deferred()
google.load 'maps', '3',
other_params: 'sensor=false'
callback: ->
apiLoaded.resolve google
apiLoaded.done (google) ->
mapInit.resolve(
new (google.maps.map)(document.getElementById('map-overlay'))
)
google
mapInit.done (map) ->
# do something with the map here for example add a marker.
new (google.maps.Marker)(
position:
lat: -25.363
lng: 131.044
map: map
title: 'Hello World!')
map
There was nothing wrong with my Coffeescript, in the end it turns out bootstrap.min.js inside my vendors folder was breaking it. Works once that file is removed.

Resources