Rails 5 - How to use javascript in production on heroku

57 views Asked by At

Note: I have to put the answer to this post in the body below. ceejayoz has flagged this post as a duplicate of one of my other attempts at this problem. It's not a duplicate and I have explained why below. However, the duplicate tag means that answers cannot be posted. See the end of the post for the answer.

I have been trying for 4 years and 4 months to figure out the basic configuration of a rails app. I am now on Rails 5 and have never been able to configure any rails app to work with javascript.

The rails guides are incomplete and out of date. The heroku guide for Rails 5 is out of date.

I have spend thousands of dollars on codementor and upwork in trying to solve this problem. I seem to be highly accomplished at throwing money away and pretty good at finding people that don't understand themselves.

So - from the top:

I have javascript that hides and shows form fields based on earlier form selections. I also have javascript that renders a google map. At the moment (and the best I have ever been able to achieve) is that I can have one or the other of these working.

In my application layout, I have:

<!DOCTYPE html>
<html>
  <head>
    <title>hello</title>
    <%= csrf_meta_tags %>
    <%= favicon_link_tag %>

    <%= 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" %>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.3.8/js/tether.min.js"></script>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>


  </head>

  <body>

    <% if user_signed_in? %>
        <%= render "layouts/nav/inner" %>
      <% else %>
        <%= render "layouts/nav/outer" %>
    <% end %>


    <p class="notice noticeContent" style='margin:0 padding: 0;'><%= notice %></p>
    <p class="alert noticeContent" style='margin:0; padding: 0;'><%= alert %></p>


    <%= yield %>


    <%= render "shared/footer" %>


     <!-- start google analytics -->
    <script>
      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
      (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
      m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
      })(window,document,'script','//www.google-analytics.com/analytics.js','ga');


    </script>
     <!-- end google analytics -->


  </body>
</html>

Points to note from the layout:

  1. I use turbolinks
  2. Part 5.2 of this rails guide says to use the following to overcome javascript problems caused by the use of turbo links

    $(document).on("turbolinks:load", function() {

  3. Previous suggestions to this problem have asked that I move the javascript include tag from the head tag into the end of the body tag. While doing this does allow me to render a google map, it prevents the hide/show javascript from working. It isn't a solution. Also, it doesnt make sense. If the reason for this change is to make sure the page is loaded before the script runs, isn't that effectively achieved by adding the line above?

In my views file, I have the js code for my google maps. It has:

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

<%= javascript_tag do %>
  $(document).on("turbolinks:load", function() {
    var addresses = <%= raw @addresses.to_json %>;
  });
<% end %>


 <script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV["GOOGLE_MAPS_API_KEY"] %>"
    async defer></script>

In my app/assets/javascripts/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);
}


<script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV["GOOGLE_MAPS_API_KEY"] %>"
    async defer></script>

This file is not in app/assets/javascripts based on advice to one of my earlier questions on this topic. It is loaded outside of the rails asset pipeline deliberately. I do not understand why.

When I try to render the view that should display a map - I don't get any console errors, but I just get a blank white space where the map should be displayed. The chrome inspector shows that the map script knows the address:

//<![CDATA[

  $(document).on("turbolinks:load", function() {
    var addresses = [{"id":1,"unit":"1","building":"Cer  Ltd","street_number":"1","street":"Pitt Street","city":"Sydney","region":"NSW","zip":"2041","country":"AU","time_zone":"Hawaii","addressable_id":1,"addressable_type":"Organisation","description":"registered_office","created_at":"2017-01-02T23:07:19.103Z","updated_at":"2017-01-02T23:07:19.103Z","latitude":"-33.858296","longitude":"151.1935757"}];
  });

//]]>

But -nothing renders.

Does anyone know where to find the rules for using javascript in a rails 5 app in the production environment on Heroku?

The Heroku guidelines for Rails 5 refer to Rails 4. The 12-factor gem is no longer useful in Rails 5 and the instructions for precompiling assets are now redundant because the heroku command to push the master branch does it automatically. I don't get any error messages when I take this step.

Is it possible for someone, relying on publicly available information, to learn how to configure a rails 5 app to use javascript (both hide/show and google maps) in production on Heroku? If so, please could you refer me to instructions.

So far, my untested hypothesis is that if I have files that are written in javascript (the map) and other files that are written in coffee script then maybe there is some inherent conflict. I cant find anything to validate this idea but I'm scratching for ideas as to why I cant get a basic app configured.

Some of my previous requests for help on this topic are here. I have tried all of the suggestions proposed: Rails - figuring out the javascript config

ceejayoz has tagged this post as a duplicate of my other post above. It is not a duplicate at all. Plainly I have set out all of the attempts made to resolve this problem. My other post (linked above and tagged as duplicated by ceejayoz) sets out all of the reasons why those suggestions do not solve the problem. Perhaps ceejayoz could justify the duplicate assessment by identifying which of the answers suggested on my other post solves the problem I raised in my post? Much as I appreciate the good intention on the part of the people that tried to help, neither of them work.

TAKING MOTTIE'S SUGGESTION

I tried Mottie's suggestion by replacing the turbo links:load line with:

<%= javascript_tag do %>
  <!-- $(document).on("turbolinks:load", function() { -->
  document.addEventListener("turbolinks:load", function() {
    var addresses = <%= raw @addresses.to_json %>;
  });
<% end %>

When I save this, push it to production and try to render the page, I get the same result as the previous attempt (without the event listener). Just a blank white space where the map should be.

I can see from the console inspector that there are no js errors. I can also see that the address is populated to the script:

//<![CDATA[

  <!-- $(document).on("turbolinks:load", function() { -->
  document.addEventListener("turbolinks:load", function() {
    var addresses = [{"id":1,"unit":"16","building":"Cr Ltd","street_number":"15","street":"Darling Street","city":"Balmain East","region":"NSW","zip":"2041","country":"AU","time_zone":"Hawaii","addressable_id":1,"addressable_type":"Organisation","description":"registered_office","created_at":"2017-01-02T23:07:19.103Z","updated_at":"2017-01-02T23:07:19.103Z","latitude":"-33.858296","longitude":"151.1935757"}];
  });

//]]> 

TRYING MOTTIE'S NEXT SUGGESTION

I understand Mottie's next suggestion to be that I should try waiting for the page to load before trying to use the address.js.

I've previously tried the suggestion in this post which I think goes to the same point. Instead of checking for on load - it's turbolinks:load in rails 5.

Mottie also suggested moving the javascript from the address view partial to the address.js. I moved this to the view on the basis of another suggestion which recommended that I load it outside of the asset pipeline - but now it's back in the address.js file (i've made a guess about where to put it).

Now I have address.js with:

document.addEventListener("turbolinks:load", function() {
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 {
      // ** this isn't any good * next step is to figure out how to write this in a js file // var addresses = <%= raw @addresses.to_json %>;
      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);
}
});

My address partial now has:

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

I moved the script tag out of the map view and to the head tag of my application layout, above the javascript include tag:

<script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV["GOOGLE_MAPS_API_KEY"] %>"
        async defer></script> 

    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.3.8/js/tether.min.js"></script>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

I cant get the assets to precompile when I try this. I tried moving the google script to the end of the address.js and get the same issue. I'm googling why that is now.

THE ANSWER

This solution came from John Sanchez on the GoRails forum. If you join that website you can see the attempts made and the problems encountered step by step. Hopefully this helps someone else.

The solution:

  1. Turn off turbo links. You can turn it back on again when you prove that this works (you turn it off by deleting require turbo links from application.js and removing turbo links: reload from the javascript include tag in the head of application layout).

  2. Move the google api call to the head tag of the application layout. Delete the async defer bit.

  3. remove the map initialiser from the app/assets/javascripts folder. Instead, put that js inside a script tag in the html view. My version of that is below. This still doesnt work for me to set the zoom level but that has something to do with using multiple addresses in a map. Ill sort that out separately. Key point to note from my earlier attempts is that the google api script isn't in the view any more and the last line of this scrip is a call to initialise the map.

    var addresses = ; function initMap() { var bounds = new google.maps.LatLngBounds(); // var map = new google.maps.Map(document.getElementById('map'), { // zoom: 5 // });
        var mapOptions = {
              zoom: 5,
              // center: {lat: 30.2669444, lng: -97.7427778},
              mapTypeId: google.maps.MapTypeId.ROADMAP
            }
    
            var mapDiv = document.getElementById('map')
    
            map = new google.maps.Map(mapDiv, mapOptions);
    
    
     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
                 });
    
        bounds.extend(address.position);
    }
    
    } map.fitBounds(bounds); } // Make sure you call the initMap function initMap();

Good luck to anyone else who is stuck on this. Please ask me questions if that's you. I've been frustrated by this for almost 5 years. Very happy to pay the help in solving this forward.

0

There are 0 answers