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

10,988

Solution 1

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.

Solution 2

Looking at how webpacker "packs" js files and functions:

/***/ "./app/javascript/dashboard/project.js":
/*! no static exports found */
/***/ (function(module, exports) {

  function myFunction() {...}

So webpacker stores these functions within another function, making them inaccessible. Not sure why that is, or how to work around it properly.

There IS a workaround, though. You can:

1) change the function signatures from:

function myFunction() { ... }

to:

window.myFunction = function() { ... }

2) keep the function signatures as is, but you would still need to add a reference to them as shown here: window.myFunction = myFunction

This will make your functions globally accessible from the "window" object.

Solution 3

Replace the code in your custom java Script file from

function function_name() {// body //}

to

window.function_name = function() {// body //}
Share:
10,988
SEJU
Author by

SEJU

Updated on June 07, 2022

Comments

  • SEJU
    SEJU almost 2 years

    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:

    • create an app_directory in /app/javascript
    • copied my development javascript file into the app_directory and renamed it to index.js
    • added console.log('Hello World from Webpacker'); to index.js
    • added import "app_directory"; to /app/javascript/packs/application.js
    • added to /config/initializers/content_security_policy.rb:

      Rails.application.config.content_security_policy do |policy|
        policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
      end
      

    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?

  • SEJU
    SEJU over 5 years
    Thanks a lot for your answer. I am playing with Rails 6.0.0 pre and since there was no JavaScript directory in assets by default, I thought that they moved the default location to JavaScript in the app directory, but I guess that directory is in fact for JavaScript framework driven sites or node, npm modules only. I recreated the JS directory in assets + application.js and now it is running. I am still curious if in Rails 6 you can pack standard JS files in the new JS directory... I was able to log to console, but not call functions ...
  • Corey Gibson
    Corey Gibson about 5 years
    Yeah, it seems with the new format in 6.0 that javascript is no longer held in the assets folder.
  • Michael Chaney
    Michael Chaney almost 5 years
    Please always look very closely when you post something like this. From the top of the linked page: "Last updated 31 December 2012" That was last updated in the Rails 3 days. Not sure if the original inquisitor can take back the green check, but this is not a correct answer for Rails 6.
  • Mark
    Mark almost 5 years
    The text I provided is from the official rails guide and applies to the latest version of rails. The link provides further explanations.
  • Michael Chaney
    Michael Chaney almost 5 years
    Rails 6 does NOT use the asset pipeline for javascript. The question specifically asks about Rails 6. Therefore, this is NOT correct. Period. Unfortunately, the guides are a bit out of date for Rails 6 in places, even the edge guides.
  • Michael Chaney
    Michael Chaney almost 5 years
    To reply to @CoreyGibson, you need to put your javascript in app/javascript/packs and let webpacker handle it. Note that this is a pretty different way of doing javascript than the asset pipeline. There are other answers here that are correct.
  • Dex
    Dex about 4 years
    This doesn't work in Rails 6. Still run into the scoping issue that @anode84 points out.
  • Alexander Gorg
    Alexander Gorg almost 4 years
    Maybe all above works, but I made things work with: stackoverflow.com/questions/59349091/…
  • Michael Chaney
    Michael Chaney almost 4 years
    @AlexanderGorg Thanks for pointing that out - I can go downvote the wrong answer.
  • code_aks
    code_aks almost 4 years
    @Michael Chaney may be your answer is correct but its too lengthy to read, please rethink about that how to answer in short way so everyone can read and implement. You can also see below answer which is short and also get more upvote then your answer just because of your answer is too lengthy. :(
  • Michael Chaney
    Michael Chaney almost 4 years
    @code_aks, the answer below is short, wrong, and much older than my correct answer. I made mine longer so that it can have a few examples of how to do this. The Rails documentation is inadequate, and since people keep ending up here this is a good place to supplement. You can upvote the wrong answer all you want, it's still wrong.
  • Alexander Gorg
    Alexander Gorg almost 4 years
    @Michael Chaney, pure console.log works, but I can't make it work as a function, i.e. call it from console.
  • Michael Chaney
    Michael Chaney almost 4 years
    @AlexanderGorg If you read what I wrote you may possibly understand this. You are thinking of the old way of doing javascript in which you define functions in your javascript files and then call them from your HTML code. The modern way of accomplishing this is to have no javascript in the HTML, so the inability to call functions from HTML doesn't matter. Please actually read what I wrote and educate yourself.
  • Alexander Gorg
    Alexander Gorg almost 4 years
    @MichaelChaney I examined the question and situation deeper and yes, I can say that you're right. Thanks for determination!
  • Alexander Gorg
    Alexander Gorg almost 4 years
    I'd only changed a bit your answer. You explain your code after, I think it's better to add the explanation as comments to the code.
  • Michael Chaney
    Michael Chaney almost 4 years
    @AlexanderGorg This is great, thanks for responding. If just one person gets it then my time has been well spent. As I said, this is a really different way of thinking about it and sometimes it's difficult to rewire your brain to think in this manner, and figuring out a workaround to keep doing things the old way won't ultimately help. I went through the same process when I started, so I'm adamant about explaining this correcty.
  • A moskal escaping from Russia
    A moskal escaping from Russia almost 4 years
    Why should people do that?
  • Michael Chaney
    Michael Chaney almost 4 years
    @SebastianPalma They shouldn't. See my correct answer. I don't know why someone came in and repeated the incorrect answer that's already here.
  • Hassam Saeed
    Hassam Saeed over 3 years
    Can you explain your method in easy wording? @MichaelChaney
  • Hassam Saeed
    Hassam Saeed over 3 years
    I think Official Documentation is not updated according to webpacker which is used in Rails 6.
  • Michael Chaney
    Michael Chaney over 3 years
    @HassamSaeed 1. It's not just "my method", it's the "correct method". 2. I have an answer here that explains it which was here and marked as accepted when you came here two days ago to copy/paste the wrong answer again. You are correct that the official docs are woefully out of date.
  • Hassam Saeed
    Hassam Saeed over 3 years
    I think my answer is not wrong. it works but it is not good approach.
  • Hassam Saeed
    Hassam Saeed over 3 years
    can you tell me the better solution for this other than using window [stackoverflow.com/questions/63328755/…
  • Michael Chaney
    Michael Chaney over 3 years
    @HassamSaeed Your answer is wrong. Please read my entire answer above. You might have to read it repeatedly to understand it (I certainly did). I'm going to answer your question that you linked above.
  • Sean
    Sean over 3 years
    This is the correct way to do it, but is also overly complicated for someone who might be sticking up a quick prototype. Webpacker is over complicated for prototyping. If you're building a commercial app, go for it and use Webpacker. If your're prototyping I would stick to Sprockets.
  • Michael Chaney
    Michael Chaney over 3 years
    @Sean It's no more complicated after you understand what you're doing. And those of us who have been developing software more than a month or two get a chuckle out of the "quick prototype - no need to get all fancy" fallacy.
  • Sean
    Sean over 3 years
    You code your way, I'll code mine. My was has gotten me well paid over the past 20 years. No need to change it now.
  • developer01
    developer01 over 3 years
    This does not work for me when I have more than one window.function_name under different names. This answer does not work.
  • Ben
    Ben over 3 years
    hey @MichaelChaney, - so we add export in front of const for our function declarations, require our file in application.js... but where are we supposed to put the import statement?
  • Michael Chaney
    Michael Chaney over 3 years
    @Ben it's a little more complicated than that. Many of your js files won't be required in application.js - you will import from them in your other js files. Looking at my "watch position" example - the first file that exports the functions doesn't need to be required in application.js. It exists only to supply those functions as imports in other js files.
  • Hassam Saeed
    Hassam Saeed over 3 years
  • Matias Carpintini
    Matias Carpintini over 3 years
    Thank you @MichaelChaney! I appreciate it
  • nepps
    nepps about 3 years
    To distill this answer down to its core for all of those finding it so hard to understand (myself included): Instead of referencing some JS function in your html, instead add a turbolinks load callback to your JS (which basically says, hey the page is loaded and ready) and inside that callback, query for your element (querySelector, getElementById, etc) and add a listener for the click there.
  • Michael Chaney
    Michael Chaney about 3 years
    @nepps That's a fundamental part of it to be sure, but the ability to import and export adds another layer to it.
  • Michael Glenn
    Michael Glenn about 3 years
    @MichaelChaney This answer was a lifesaver! I've muddled poorly through Javascript as it's evolved and admittedly I've been lazy as I don't like it as a language. When I transitioned to Webpacker I couldn't understand why so many things were breaking and why so many install tutorials were assigning modules to window. Definitely a game changer. Will be spending more time learning to make sure I've got it down pat.
  • Peter Gerdes
    Peter Gerdes almost 3 years
    This is deliberate as webpacker is encouraging you to use the new module functionality so you don't get name collisions etc. Chaney's reply explains how to do this correctly.
  • ricks
    ricks almost 3 years
    Great answer, i had to dive through hours of random garbage to find this gem. Appreciate the help.
  • ricks
    ricks almost 3 years
    Please do not use this solution because you don't want to read Chaney's answer, if you want to do it the right way, spend a few minutes understanding Chaney's answer.