4

I have been struggling for 4 years to figure out how I can use google maps in my Rails app.

I've tried to use gmaps4rails but given up because it's too hard. I managed to find a solution on SO, which worked except that i couldn't specify a zoom level. However, in production, all of my other javascripts didn't work. The solution for that was to move the:

<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

out of the head tag and to the bottom of the body tag.

That works to let all of my other js files operate as they do in the development environment, when in production mode, however, now the map won't render at all.

Is there a solution which:

  1. allows me to use my javascript files (other than the google maps js file)- which I think is achieved by having the javascript include tag at the end of the body instead of the head

  2. allows me to render a google map in production? If I move the javascript include tag back to the head I can render the map, but the rest of my js files don't work (in production only).

  3. allows me to specify a zoom level on my map?

I have tried to ask for help here, here and here (and a million other times on this board). I haven't managed to find help yet.

In my address.js, I have:

function initMap() {
  var map = new google.maps.Map(document.getElementById('map'), {
    zoom: 5
  });
  var bounds = new google.maps.LatLngBounds();
  // var opts = {
  //   zoom: 10,
  //   max_zoom: 16
  // }

  var n = addresses.length;
  for (var i = 0; i < n; i++) {
    var lat = addresses[i].latitude;
    var lng = addresses[i].longitude;
    if (lat == null || lng ==null){
      console.log(addresses[i].name + " doesn't have coordinates");
    }else {
      var address = new google.maps.Marker({
        position: {lat: parseFloat(lat), lng: parseFloat(lng)},
        title: addresses[i].name,
        map: map //,
//        zoom: 8
      });
      bounds.extend(address.position);
    }
  }
  map.fitBounds(bounds);
}

As I outlined in this post, I get an error that says:

initMap is not a function

I've tagged all the relevant posts that suggest solutions in that linked post, together with the results of my efforts to use those solutions to solve my problem.

In relation to the zoom level on the map, the answer in this post explained the problem as being attributable to the js asking for a map with each address in it. That somehow forces the zoom level to be the most zoomed in, regardless of the level I try to specify. So there are 2 issues, the first is how to remove the bit that forces the most zoomed in level so that I can set a zoom level that will be acknowledged and the second is whether there has been some change in the way to express the zoom level, because so many answers on SO (including those linked in my post) suggest using "setZoom" instead of "zoom: 5". I can't find a solution that worked.

As I outlined in this post, the js files (except for google maps) don't work in production if I have the javascript include tag in the head. I only arrived at this solution after a long, expensive session on codementor. I can't afford another session to find out how to solve the problems that this solution has created. I also didn't get an explanation as to the fundamental reason why this change is necessary. The rails guides suggest including it there so I don't understand why, for the most part, moving it to the end of the body tag is a solution that gets the js to work in the production environment. This breaks the address js though, so it isn't the right solution

In my config.rb I have

config/initializers/assets.rb

# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'

# Add additional assets to the asset load path
# Rails.application.config.assets.paths << Emoji.images_path

# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
# Rails.application.config.assets.precompile += %w( search.js )
config/production.rb

  # Do not fallback to assets pipeline if a precompiled asset is missed.
  config.assets.compile = false

I have tried each of the solutions posted in the linked posts (which are mostly from others who have posted on this board. I would love to learn the principle involved in figuring this out (so that I can begin to learn to think the way that rails wants me to). I'm baffled by why this is so difficult and why so little is written about configuring apps to work in production in any of the texts. There is clearly a problem that needs a solution (I have combed through 100s of other posts on this topic, but cannot find a principled answer) or a solution that works.

If it makes any difference, my production environment is on heroku. Any advice, for principles that are relevant would be very much appreciated - if a direct answer is not immediately apparent.

TAKING R_G'S SUGGESTION

I changed my address.js file to include a new first line and a new last line. That file now has:

;(function ready() {
function initMap() {
  var map = new google.maps.Map(document.getElementById('map'), {
    zoom: 5
  });
  var bounds = new google.maps.LatLngBounds();
  // var opts = {
  //   zoom: 10,
  //   max_zoom: 16
  // }

  var n = addresses.length;
  for (var i = 0; i < n; i++) {
    var lat = addresses[i].latitude;
    var lng = addresses[i].longitude;
    if (lat == null || lng ==null){
      console.log(addresses[i].name + " doesn't have coordinates");
    }else {
      var address = new google.maps.Marker({
        position: {lat: parseFloat(lat), lng: parseFloat(lng)},
        title: addresses[i].name,
        map: map //,
//        zoom: 8
      });
      bounds.extend(address.position);
    }
  }
  map.fitBounds(bounds);
}
})();

When I start the server and try to render the page, the map still does not render (just an empty white space where it should be). There are no console errors showing in the chrome inspector (just as there are not when I do not use the first and last lines suggested by R_G).

RESPONSE TO ENGINEER DAVE In response to your questions:

Q1: Are your production assets being precompiled?

No - the reason for that is that my production.rb notes tell me not to. See here:

# Do not fallback to assets pipeline if a precompiled asset is missed.
  config.assets.compile = true

  # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb

I understand these notes to mean that assets.rb handles the precompilation, so there is no longer a need to precompile production assets. The first line of the instructions that I copied above tells me not to fallback to the assets pipeline so I'm not sure what you're expecting to see differently.

In asset.rb, the notes say:

# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.# Do not fallback to assets pipeline if a precompiled asset is missed.

I understand this to mean everything listed in application.js is already handled so there is no need for further recompilation steps. I understand (from previous attempts over the years to solve this problem that doubling up on the compilation is a cause of a separate set of problems).

My address.js file is listed in my application.js file (via the require tree), so it should be dealt with by whatever the process that assets.rb uses.

Q2. Are you requiring the tree in application.js

Yes. I am.

To your comment: "Also if you're not requiring the whole JS tree of files in application.js then the files you have flagged for pre-compilation in assets.rb, e.g. address.js, will get precompiled but will not be loaded."

My application.js has:

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require jquery-fileupload/vendor/jquery.ui.widget
//= require jquery-fileupload/jquery.iframe-transport
//= require jquery-fileupload/jquery.fileupload
//= require bootstrap-sprockets
//= require moment
//= require bootstrap-datetimepicker
//= require pickers
// require underscore
// require gmaps/google
//= require markerclusterer
//= require cocoon
//= require cable
//= require_tree .

I am requiring the tree. Address.js is in my app/assets/javascripts folder so it should be picked up by the require_tree line at the end of the list in application.js

I'm not sure I've understood any of your comment. Is there a suggestion that I could try to get this working? Is your suggestion to ignore the instructions in the assets.rb and production.rb setup?

What is the difference between 'precompiled' and 'loaded'? I'll try to do some research to understand what that means now.

ENGINEERDAVE'S COMMENT ON R_G'S SUGGESTION

EngineerDave suggests referring to this post as a potential solution. This post suggests:

  1. moving the javascript include tag to the end of my body tag (which I have done, with the effect that the google map no longer works but the hide /show js in the other file that wasn't working now does work).

  2. using this line:

    document.addEventListener("DOMContentLoaded", function(event) { //do work });

That post goes on to suggest a simpler implementation of that line, as:

An actual working plain javascript implementation here if someone wants code they can just drop in: stackoverflow.com/questions/9899372/… – jfriend00 Dec 13 '14 at 7:58

That link, suggests:

<script>
// self executing function here
(function() {
   // your page initialization code here
   // the DOM will be available here

})();
</script>

My current code in address.js begins with this line:

function initMap() {

Taking the suggestion in this post, I try replacing that line with:

(function() {
  function initMap() {
   //then continue with the rest of my address.js file
})();

When I save this and try it, I get the same outcome as when I tried R_G's first suggestion. No map. No console errors showing in the chrome inspector.

THE DOWNVOTE

I don't understand the reason for the down vote on this question. I have been struggling for years to try to learn how to work with rails with javascript. I have spent thousands of dollars on codementor.io/ upwork and a bunch of the other sites trying to pay for professional help to learn this. I've been to every meetup in my city and make a point to try them in different cities when I have the opportunity to do so. I haven't found a person who has been able to figure this out. 4+ years and I'm still trying to solve this. What's the reason to down vote this. It doesn't give me any insight as to why this forum is not the right place to seek help. I'm out of options for alternatives. I just don't have any more budget to waste on codementor.io. The down vote only costs me points which slows me down from being able to start another bounty on this forum to try and find some information that will help to solve this problem. If you have constructive feedback on how better to ask this question or do the research necessary to find the answer - either of those things would be gratefully appreciated. Have a nice day.

To everyone else, thank you for trying to help me.

BRENZY'S SUGGESTION

Thank you so much for taking the time to make the repo. It's so helpful to see someone else lay this out. That said, this doesnt work for me. I had to adapt some of your approach to get rid of some of the errors this made for me. I've set out how I incorporated your suggestion and the error it gives below:

Application.html.erb

Changes:

  1. I moved the javascript include tag back to the head tag

  2. Above the js include tag, I put the google source link. I've shown the link you suggested in comment below, and the link I used beneath it. I had to change the link because I got an error saying no API keys using your link.

    <%= csrf_meta_tags %>
    <%= favicon_link_tag %>
    <!-- <script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script> -->
    <script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV["GOOGLE_MAPS_API_KEY"] %>"async defer></script>
    
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <!-- Latest compiled and minified CSS -->
    <%= stylesheet_link_tag  href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css" %>
    
    <!-- Optional theme -->
    <%= stylesheet_link_tag href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap-theme.min.css" %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    

app/assets/javascripts/initialise.js

I made a new file - like yours.

function initialize() {
  var losAngeles = new google.maps.LatLng(34.0500, -118.2500);
  var pasadena = new google.maps.LatLng(34.14778, -118.14452);
  var mapOptions = {
    zoom: 10,
    center: losAngeles,
    disableDefaultUI: true,
    mapTypeControlOptions: google.maps.MapTypeId.SATELLITE
  };
  map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
}

google.maps.event.addDomListener(window, 'load', initialize);

The current error is pointing at the last line of this file.

One thing I don't understand from your commentary is that you think this file is outside of the asset pipeline. I don't understand that because your application.js has require_tree, which I understand to mean that everything in app/assets/javascripts gets included in the pipeline.

My view is now the same as yours:

<div id="map-canvas"></div>

When I save all of this and try it, I get an error that says:

Uncaught ReferenceError: google is not defined
    at initialize.self-f919dc6….js?body=1:13
(anonymous) @ initialize.self-f919dc6….js?body=1:13

There is just a blank white space instead of a map.

THE BIGGER PROBLEM

I pushed this to heroku and tried the production version. Now, the js that hides & shows form fields based on other form selections doesnt work. This was the original problem that led to putting the javascript include tag at the end of the body instead of the head tag.

That js is in a file called app/assets/javascripts/stance/commercial.js

jQuery(document).ready(function() {
   jQuery('[name="organisation[commercial_attributes][standard_financial_split]"]').on('click', function() {
      if (jQuery(this).val() == 'true' ) {
          jQuery('#commercial_standard_financial_split_content').removeClass('hidden');
      } else {
          jQuery('#commercial_standard_financial_split_content').removeClass('hidden').addClass('hidden');
      }
   });

});

This did work when the javascript include tag was moved to the end of the body tag. Now that it's back in the head tag - it doesnt work.

Community
  • 1
  • 1
Mel
  • 3,699
  • 13
  • 71
  • 194
  • 4
    "I have been struggling for 4 years..." Bro? – Arslan Ali Dec 11 '16 at 02:44
  • My first attempts were over the first 3.5 years - during which time i was trying to figure out how to use gmaps4rails in my app. It's too difficult to learn. There is assumed knowledge in all of the tutorials that doesnt get expressed anywhere. I gave up on trying to decipher it. I found a solution without that gem, but now I can't get it to work if my js is at the end of the body tag and either way, I can't get a zoom level to work. – Mel Dec 11 '16 at 02:46
  • Hey there! I had already work with rails and gmaps, and everything works fine, including zoom. I will look at source code at evening. If I won't respond in 24 hours ping me here. – unkmas Dec 13 '16 at 11:00
  • please add the generated html code of your page stripping everything besides `html`, `body` and `script` tags – Dennis Baizulin Dec 18 '16 at 14:30
  • @Dennis how can I do that? – Mel Dec 18 '16 at 21:39
  • Are your production assets being precompiled? Your posted config has the fallback disabled so working in development and not in production would be the expected result w/o a successful precompile. Also if you're not requiring the whole JS tree of files in application.js then the files you have flagged for pre-compilation in assets.rb, e.g. address.js, will get precompiled but will not be loaded. Hence the functions they contain will not be available. You should be able to check if this function is loaded in chrome developer console by typing initMap() in the JS console. – engineerDave Dec 19 '16 at 19:28
  • @Mel engineerDave corrected my error. Hope he can help you. – Richard_G Dec 19 '16 at 20:01
  • @engineerDave - thanks for the thoughts. I posted a response at the end of the post above – Mel Dec 19 '16 at 20:33
  • @R_G - thanks R_G. I'm not sure how to construe engineerDave's thoughts. I do require tree and I don't precompile assets. The reasons for why are set out above. – Mel Dec 19 '16 at 20:34
  • per your posted application.js you have "// require gmaps/google" this is a comment in sprockets. You need "//= require gmaps/google" to actually require it. Note the equal sign. – engineerDave Dec 20 '16 at 17:17
  • also note you can check if a function is available in the window via chrome developer mode's JS console. just type the function name at the prompt. if its available it'll autocomplete or explicitly search for it like this http://stackoverflow.com/a/10912314/793330 – engineerDave Dec 20 '16 at 17:28
  • @engineerDave - that file is commented deliberately. I was using it when i was trying to figure out gmaps4rails gem but I commented that file when I gave up on that pursuit and instead used the js in address.js which is included by the require tree line. – Mel Dec 20 '16 at 19:09
  • thanks for the search tip. I'll try it now – Mel Dec 20 '16 at 19:09
  • @Mel - did you see my answer below? – brenzy Dec 20 '16 at 19:25
  • @brenzy - yes. Thank you! I'm trying it now. I'll come back in a few minutes. – Mel Dec 20 '16 at 20:45

2 Answers2

2

EDIT: See update at the end for a more detailed explanation, but I just saw what is your most likely error. If you follow my recommendation, it should certainly help. I assumed that you only pasted in one function of your code, not your entire file. It is (most often) important in JavaScript to ensure that the entire page has loaded before your code begins to run. That is the purpose of the "function(){...}" that I recommended. It waits until the document is loaded before script execution starts. Without that, you will likely see spurious errors in your scripts, like "initMap is not a function". Follow my recommendation and see if that helps.


You could already be doing this, so let me know if that is true but...

If your results vary according to placement within the scripts, my thought would be that you are polluting the global namespace and suffering conflicting variables as a result. It's much safer to wrap all your code such that it's structure is local and does not impact the global.

Pure JavaScript would have this pattern:

;(function() {
    // Document is ready here
})();

I use jQuery, and so I do it this way:

;$(function ($, window, document) {
    // Document ready here
}(window.jQuery, window, document));

Exactly how you want to do this depends upon your own pattern, but the goal is to not pollute the global environment.

Hey, it's at least worth a shot.


UPDATE: Since you asked and I referred you to the first suggestion, let me parse that out for you. The code to use is:

;(function() {
    // Document is ready here
})();

Replace "// Document is ready here" with your current code. If you have multiple files containing your current code, do the same for each.

The first semicolon is just there to terminate any preceding code, in case they forgot to terminate it. The parenthesis after the semicolon is the key to the local isolation. It pairs with the first parenthesis in the last line. Any variables or functions that you define within those parenthesis will be isolated from anything outside of them. The last two paired parenthesis on the last line executes all of the code as a function, therefore instantiating your internal code. Without that, your code would be inaccessible.

Richard_G
  • 4,302
  • 2
  • 35
  • 73
  • It's not placement within the scripts that is varied. It's where the include tag is placed that seems to impact the application of the js – Mel Dec 15 '16 at 23:37
  • @Mel Yes, the include tag "Returns an HTML script tag for each of the sources provided." Moving it changes the locations of the specified sources in relation to the rest of the file. Either way, avoiding pollution of the global environment is a recommended pattern and, to my mind, worth a shot. YMMV. – Richard_G Dec 15 '16 at 23:59
  • I don't know how to try your suggestion. The opening line of my address.js is: function initMap() { Are you suggesting that I add ";$" to the beginning of the opening line? and add the "($, window, document) to the end of the opening line and then "(window.jQuery, window, document)"? I tried that but it gives a syntax error which I don't know how to debug given I don't understand the construct of it. – Mel Dec 18 '16 at 01:08
  • @Mel It appears you are using native JavaScript, so the first answer would apply. Just take all of your current code and drop it to replace "// Document is ready here". Then test. It may help. It may not. But, the idea is to reduce or remove the likelihood of conflict between your code and any other code. – Richard_G Dec 18 '16 at 17:04
  • I tried your suggestion and copied the code to the post above. It didn't work. It didnt change anything compared to not using the new first and last lines. Thanks anyway for trying to help me. – Mel Dec 18 '16 at 21:38
  • Note ready is a jQuery function. I think your answer is misleading as I'm not sure why you are alluding that there is a pure JS ready function called "ready". For non jQuery app use this answer http://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery – engineerDave Dec 19 '16 at 19:38
  • @engineerDave Yes, I see you are right. I thought I had it correct, but like I said, I do use jQuery. My error. Will update. – Richard_G Dec 19 '16 at 19:59
  • I tried following the suggestion in the post EngineerDave suggested. As well as the suggestions linked in that post. I've not been able to get them to work to solve this problem. I've set out my best attempt above. Thanks both for trying to help me. – Mel Dec 19 '16 at 20:54
1

I have created an extremely simple Rails 5 app at https://github.com/brenzy/sample1 that loads a google map in both dev and production environments.

1) I added the google maps script outside of the asset pipeline and made sure it was above the application.js include:

<script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>

2) As per EngineerDave, I used the following line to make sure the map doesn't get initialized until it is loaded.

google.maps.event.addDomListener(window, 'load', initialize);

I didn't touch any of the default configurations settings.

Hopefully this is along the lines of what you are after, and you can compare it to what your code is doing.

brenzy
  • 1,906
  • 11
  • 18
  • Hi @brenzy Thanks so much for making the repo. I tried your suggestion and set out the attempt at the end of the post above. Unfortunately it didnt work to render a map, but it also breaks the other js (that other js was working when the javascript include tag was at the end of the body tag instead of the head tag). – Mel Dec 20 '16 at 21:32
  • @Mel - I'm sorry that this didn't work for you. If I run the code in the repo I see a map. If this was my problem, I would start from scratch with just displaying a map, without bootstrap, and without any css. If you add back the javascript components one at a time it might give you a clue as to where the incompatibility lies. It doesn't make sense that google is not defined if initialize is only called from google.maps.event.addDomListener. I would also put some console.log statements in to see where the error is coming from, as well as stepping through the uglified code. – brenzy Dec 21 '16 at 00:47
  • Thanks very much for the suggestion. I'm afraid I don't know what to look for as indicators of incompatibility. I've done the code school javascript course about 3 times but it isn't much good for trying to figure out these sort of problems. I made a new app about 6 months ago to try to solve this specific problem, but I've just spent 6 months trying to move lines around into different orders to see if guessing will help me stumble upon an answer. I'll keep going with that approach, I suppose. Thank you so much for taking the time to make that repo. I appreciate the effort. – Mel Dec 21 '16 at 00:50
  • To me it sounds like a JS load order issue, i.e. the JS function you're calling "google" isn't present when you try to call it. Because you're loading google maps asynchronously it would seem you can't rely on hooking into it via a document callback. See this answer http://stackoverflow.com/a/14185834/793330 So write your initialize function as you have and call it via a passed in callback parameter when you load the google maps JS (add &callback=initialize to the end of your google maps js loading line per the answer) – engineerDave Dec 21 '16 at 18:02