loading...

Adding Custom JavaScript in Rails 6

morinoko profile image Felice Forby Updated on ・3 min read

In Rails 6, JavaScript assets are no longer included with the app/assets/ directory and instead have been moved into a separate directory app/javascript handled by Webpacker. That's great, but when I wanted to add some custom javascript of my own, there wasn't any clear documentation that described how it should be done.

After some experimentation and digging around on the internet, here are a couple of methods that seem to work. Note that I'm not a javascript expert by any means, so if there is a better way—or other ways that I'm missing—let me know in the comments!

Method 1: Create a custom directory and require it in application.js

If you look at application.js, the comment at the top suggests that you "place your actual application logic in a relevant structure within app/javascript and only use these pack files to reference that code so it'll be compiled."

// app/javascript/packs/application.js

// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

You can set this up by adding a custom directory within the app/javascript/, e.g. custom/, and then require the files within in application.js.

For example, if you have a javascript file named home.js in the app/javascript/custom/ directory, you can get it to load with require():

// app/javascript/packs/application.js

// ...

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

require("custom/home")

Since it's required in application.js, the custom javascript will be compiled along with all the other javascript into a timestamped application.js file that looks something like application-a03d1868c0a3f825e53e.js.

This is loaded by the <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> tag in the app/views/layouts/application.html.erb, automatically created when you make a new Rails app.

For a cleaner look, you could also name the javascript file index.js and require it with a simple reference to the directory like require("custom") because require() looks for an index.js file when given a path to a directory (if there is no index.js, it will fail).

Method 2: Add custom JavaScipt within the app/javascript/packs directory

If for some reason you don't want to create another directory, you could opt to add custom javascript files within the app/javascript/packs directory. Then, require the files in application.js with require().

For example, if you have a file named custom.js in app/javascript/packs, require it like below and it will be compiled into the timestamped application.js file with all the other javascript:

// app/javascript/packs/application.js

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

require("packs/custom")

Method 3: Using javascript_pack_tag for separate JavaScript files (packs)

If you don't want your custom javascript to be compiled into the application.js with everything else, you can get Rails to compile it into a separate file, or "pack."

To do so, add a custom file within app/javascript/packs, e.g. custom.js, then use the javascript_pack_tag helper where the file is needed in the views like so: <%= javascript_pack_tag 'custom' %>

The custom javascript will be compiled separately from the rest of the javascript into a timestamped custom.js that looks something like this: custom-a03d1756c0a3f825e53e.js

Here's how it might look if you added the custom javascript just before the ending body tag in the layouts/views/application.html.erb:

# app/layouts/views/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body class="<%= controller_name %> <%= action_name %>">
    <%= render 'layouts/header' %>

    <%= yield %>

    <%= render 'layouts/footer' %>

    <%= javascript_pack_tag 'custom' %>
  </body>
</html>

Thanks for reading!

Posted on by:

morinoko profile

Felice Forby

@morinoko

Former Japanese translator turned full-stack we engineer. I work mostly with Ruby and Rails, and love working on both the back-end and the front-end. I'm based in Japan :)

Discussion

markdown guide
 

Method 2 is horrible. I tried is personally and its hard to believe that JS files are not compiled according to the order given but with alphabetical naming of those files.

so in case your file requires jquery and has name custom.js, with following order:

require("jquery")
require("packs/custom")

It first compiles custom.js then jquery.

 

Have you found any method for including js from node_modules in order? Or is the replacement for the asset pipeline just useless in Rails 6 because we can't define the order in which js is included?

 

Why you have to include? Just use yarn add package_name to add js library and require or import the global variable.

I meant require when I said include. The webpack puts the JS in the wrong order and it doesn't work when I do a regular require.

 

Thanks for the article. Any way to add coffee files? I tried require("custom/pagination") and require("custom/pagination.coffee") but I get the following error: Error: Cannot find module 'custom/pagination.coffee'`

 

try require("./custom/pagination")

 

Apparently Coffee scripts are not supported by Rails 6.

 

Thanks for this article @morinoko ! Just started up a Rails 6 application and couldn't figure out where to put the JavaScript files. Now I know thanks to this article.

 

Thanks again for reading :D Good luck with your new application!

 

Thanks for the article! I try to call JavaScript 'alert' when clicking the button:

  • JavaScript code is placed in app/javascript/packs/custom.js and contains only this function: function dosmth() { alert(‘hello’); }
  • This file is referenced in app/views/layouts/application.html.erb: <%= javascript_pack_tag ‘custom’ %>
  • Finally, JS code is called on button click in a ERB file: <%= button_tag ‘Do smth’, type: ‘button’, :onclick => “dosmth()” %> When I run the app, I see a compiled script on a page. But when I click the button, I get the error: ReferenceError: Can’t find variable: dosmth. Could you please help if you have any ideas? inspect
 

Well, I did some digging... Since these are webpack modules, I guess you have to do a little more work to make the function available if you're simply declaring it in pack/custom.js. First, you need to export function by adding an export at the end of the file:

// javascript/packs/custom.js

function dosmth() { 
  alert('hello'); 
}

export default dosmth;

Then, in the main application.js, you have to import it and make it available to the window. You can do that by adding this to the bottom of application.js

// javascript/packs/application.js

// require statements, etc....

import dosmth from './custom'
window.dosmth = dosmth

Then it should work! I think there's a way to export and import all functions in a file too... I'm not very familiar with how webpack works yet, but I'll try to update this post with some more info soon.

Instead of exporting/importing, another way is to add the onClick function with an event listener instead. First, give your button and id or something like that and remove the onclick:

<%= button_tag 'Do smth', id: 'button-click', type: 'button' %> 

Then in the custom.js file, add an event listener:

// javascript/packs/custom.js

function dosmth() { 
  alert('hello'); 
}

document.addEventListener('turbolinks:load', () => {  
  const clickButton = document.getElementById("button-click");  

  clickButton.addEventListener("click", dosmth); 
});

You do need to make sure you wrap the event listener (and other JS) inside the document.addEventListener('turbolinks:load', () => {}. That's Rails' way of making sure the page and turbolinks is loaded before any of the javascript gets executed!

I hope that helps!! I learned a bit trying to figure this out too!

 

Thanks for this article. It was helpful. Changing the declaration of your JavaScript function may simplify things a bit:

// in your html
<a href="#" onclick="dosmth()">dosmth</a>


// javascript/packs/custom.js
window.dosmth = function() {
  alert('hello');
}

// I don't think you need the export default dosmth;
// export default dosmth;


// In your javascript/packs/application.js I don't think you need the corresponding declarations:

// import dosmth from './custom'
// window.dosmth = dosmth

 
 

I am currently working on Action Singal-ing the connection for a webchat. Credits to Jean Paul Sio's work. His worked for in Rails 5 but Rails 6 is a new different ballgame. Two functions have been getting reference errors namely ()handleJoinSession and ()handleLeaveSession
**in the packs/signaling-server.js these two functions are syntaxed this way:

const handleJoinSession = async () => {
App.session = await App.cable.subscriptions.create("SessionChannel", {
connected: () => {
broadcastData({
type: JOIN_ROOM,
from: currentUser
});
},
received: data => {
console.log("received", data);
if (data.from === currentUser) return;
switch (data.type) {
case JOIN_ROOM:
return joinRoom(data);
case EXCHANGE:
if (data.to !== currentUser) return;
return exchange(data);
case REMOVE_USER:
return removeUser(data);
default:
return;
}
}
});
};

const handleLeaveSession = () => {
for (user in pcPeers) {
pcPeers[user].close();
}
pcPeers = {};

App.session.unsubscribe();

remoteVideoContainer.innerHTML = "";

broadcastData({
type: REMOVE_USER,
from: currentUser
});
};

const joinRoom = data => {
createPC(data.from, true);
};

const removeUser = data => {
console.log("removing user", data.from);
let video = document.getElementById(remoteVideoContainer+${data.from});
video && video.remove();
delete pcPeers[data.from];
};

While in the views section they are called through a button click

like wise with

I was able to access camera and have something pop out by simply adding
import handleJoinSession from './signaling-server' or require("packs/signaling-server") in the application.js (following your advise). I can also make the camera work just by the javascript tag in the views but still the reference error occurs. The perfect scenario would be these function be referenced so that the camera would turn on as with the handleJoinSession and off with the handleLeaveSession occurs. Anyone well versed with the javascript syntax, I would like to thank in advance. :)

Here is the exact error: (index):1 Uncaught ReferenceError: handleLeaveSession is not defined
at HTMLButtonElement.onclick (VM73 :1)

 

how to add bundle install gems to application.js file since some gems for available on yarn. in rails 6