javascriptruby-on-railswebpackwebpackerruby-on-rails-6

Rails 5/6: How to include JS functions with webpacker?


I am trying to update a Rails 3 app to Rails 6 and I have problems with the now default webpacker since my Javascript functions are not accessible.

I get: ReferenceError: Can't find variable: functionName for all js function triggers.

What I did is:

I get 'Hello World from Webpacker' logged to console, but when trying to access a simple JS function through <div id="x" onclick="functionX()"></div> in the browser I get the reference error.

I understand that the asset pipeline has been substituted by webpacker, which should be great for including modules, but how should I include simple JS functions? What am I missing?

Thanks in advance.


Solution

  • For instructions on moving from the old asset pipeline to the new webpacker way of doing things, you can see here:

    https://www.calleerlandsson.com/replacing-sprockets-with-webpacker-for-javascript-in-rails-5-2/

    This is a howto for moving from the asset pipeline to webpacker in Rails 5.2, and it gives you an idea of how things are different in Rails 6 now that webpacker is the default for javascript. In particular:

    Now it’s time to move all of your application JavaScript code from app/assets/javascripts/ to app/javascript/.

    To include them in the JavaScript pack, make sure to require them in app/javascript/pack/application.js:

    require('your_js_file')
    

    So, create a file in app/javascript/hello.js like this:

    console.log("Hello from hello.js");
    

    Then, in app/javascript/packs/application.js, add this line:

    require("hello")
    

    (note that the extension isn't needed)

    Now, you can load up a page with the browser console open and see the "Hello!" message in the console. Just add whatever you need in the app/javascript directory, or better yet create subdirectories to keep your code organized.


    More information:

    This question is cursed. The formerly accepted answer is not just wrong but grotesquely wrong, and the most upvoted answer is still missing the mark by a country mile.

    anode84 above is still trying to do things the old way, and webpacker will get in your way if you try that. You have to completely change the way you do javascript and think about javascript when you move to webpacker. There is no "scoping issue". When you put code in a web pack it's self-contained and you use import/export to share code between files. Nothing is global by default.

    I get why this is frustrating. You're probably like me, and accustomed to declaring a function in a javascript file and then calling it in your HTML file. Or just throwing some javascript at the end of your HTML file. I have been doing web programming since 1994 (not a typo), so I've seen everything evolve multiple times. Javascript has evolved. You have to learn the new way of doing things.

    If you want to add an action to a form or whatever, you can create a file in app/javascript that does what you want. To get data to it, you can use data attributes, hidden fields, etc. If the field doesn't exist, then the code doesn't run.

    Here's an example that you might find useful. I use this for showing a popup if a form has a Google reCAPTCHA and the user hasn't checked the box at the time of form submission:

    // For any form, on submit find out if there's a recaptcha
    // field on the form, and if so, make sure the recaptcha
    // was completed before submission.
    document.addEventListener("turbolinks:load", function() {
      document.querySelectorAll('form').forEach(function(form) {
        form.addEventListener('submit', function(event) {
          const response_field = document.getElementById('g-recaptcha-response');
          // This ensures that the response field is part of the form
          if (response_field && form.compareDocumentPosition(response_field) & 16) {
            if (response_field.value == '') {
              alert("Please verify that you are not a robot.");
              event.preventDefault();
              event.stopPropagation();
              return false;
            }
          }
        });
      });
    });
    

    Note that this is self-contained. It does not rely on any other modules and nothing else relies on it. You simply require it in your pack(s) and it will watch all form submissions.

    Here's one more example of loading a google map with a geojson overlay when the page is loaded:

    document.addEventListener("turbolinks:load", function() {
      document.querySelectorAll('.shuttle-route-version-map').forEach(function(map_div) {
        let shuttle_route_version_id = map_div.dataset.shuttleRouteVersionId;
        let geojson_field = document.querySelector(`input[type=hidden][name="geojson[${shuttle_route_version_id}]"]`);
    
        var map = null;
    
        let center = {lat: 36.1638726, lng: -86.7742864};
        map = new google.maps.Map(map_div, {
          zoom: 15.18,
          center: center
        });
    
        map.data.addGeoJson(JSON.parse(geojson_field.value));
    
        var bounds = new google.maps.LatLngBounds();
        map.data.forEach(function(data_feature) {
          let geom = data_feature.getGeometry();
          geom.forEachLatLng(function(latlng) {
            bounds.extend(latlng);
          });
        });
        map.setCenter(bounds.getCenter());
        map.fitBounds(bounds); 
      });
    });
    

    When the page loads, I look for divs with the class "shuttle-route-version-map". For each one that I find, the data attribute "shuttleRouteVersionId" (data-shuttle-route-version-id) contains the ID of the route. I have stored the geojson in a hidden field that can be easily queried given that ID, and I then initialize the map, add the geojson, and then set the map center and bounds based on that data. Again, it's self-contained except for the Google Maps functionality.

    You can also learn how to use import/export to share code, and that's really powerful.

    So, one more that shows how to use import/export. Here's a simple piece of code that sets up a "watcher" to watch your location:

    var driver_position_watch_id = null;
    
    export const watch_position = function(logging_callback) {
      var last_timestamp = null;
    
      function success(pos) {
        if (pos.timestamp != last_timestamp) {
          logging_callback(pos);
        }
        last_timestamp = pos.timestamp;
      }
    
      function error(err) {
        console.log('Error: ' + err.code + ': ' + err.message);
        if (err.code == 3) {
          // timeout, let's try again in a second
          setTimeout(start_watching, 1000);
        }
      }
    
      let options = {
        enableHighAccuracy: true,
        timeout: 15000, 
        maximumAge: 14500
      };
    
      function start_watching() {
        if (driver_position_watch_id) stop_watching_position();
        driver_position_watch_id = navigator.geolocation.watchPosition(success, error, options);
        console.log("Start watching location updates: " + driver_position_watch_id);  
      }
    
      start_watching();
    }
    
    export const stop_watching_position = function() {
      if (driver_position_watch_id) {
        console.log("Stopped watching location updates: " + driver_position_watch_id);
        navigator.geolocation.clearWatch(driver_position_watch_id);
        driver_position_watch_id = null;
      }
    }
    

    That exports two functions: "watch_position" and "stop_watching_position". To use it, you import those functions in another file.

    import { watch_position, stop_watching_position } from 'watch_location';
    
    document.addEventListener("turbolinks:load", function() {
      let lat_input = document.getElementById('driver_location_check_latitude');
      let long_input = document.getElementById('driver_location_check_longitude');
    
      if (lat_input && long_input) {
        watch_position(function(pos) {
          lat_input.value = pos.coords.latitude;
          long_input.value = pos.coords.longitude;
        });
      }
    });
    

    When the page loads, we look for fields called "driver_location_check_latitude" and "driver_location_check_longitude". If they exist, we set up a watcher with a callback, and the callback fills in those fields with the latitude and longitude when they change. This is how to share code between modules.

    So, again, this is a very different way of doing things. Your code is cleaner and more predictable when modularized and organized properly.

    This is the future, so fighting it (and setting "window.function_name" is fighting it) will get you nowhere.