DEV Community

Felice Forby
Felice Forby

Posted on • Updated on

Adding Custom JavaScript in Rails 6

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Thanks for reading!

Top comments (19)

Collapse
 
rajanverma_me profile image
Aarvy

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.

Collapse
 
vincenttaglia profile image
Victor Vincent Taglia • Edited

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?

Collapse
 
rajanverma_me profile image
Aarvy • Edited

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

Thread Thread
 
vincenttaglia profile image
Victor Vincent Taglia

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.

Collapse
 
duuusha23 profile image
Andrew Aks • Edited

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
Collapse
 
morinoko profile image
Felice Forby • Edited

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' %> 
Enter fullscreen mode Exit fullscreen mode

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); 
});
Enter fullscreen mode Exit fullscreen mode

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!

Collapse
 
johnviviano profile image
John Viviano

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

Collapse
 
duuusha23 profile image
Andrew Aks

Ooh, great thanks, Felice!

Collapse
 
nicolrx profile image
nico_lrx

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'`

Collapse
 
maysam profile image
Maysam Torabi

try require("./custom/pagination")

Collapse
 
nicolrx profile image
nico_lrx

Apparently Coffee scripts are not supported by Rails 6.

Collapse
 
colocodes profile image
Damian Demasi

Awesome post! I ended up using a variation of the method 3:

Using include_javascript_pack 'my_module' in an index.html.slim view file.

Creating a app/javascript/my_module/index.js file with the actual JS code.

Creating a app/javascript/packs/my_module.js pack that imports the JS code: import 'my_module';.

Then I ran: RAILS_ENV=development bundle exec rails webpacker:compile.

Collapse
 
michael profile image
Michael Lee 🍕

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.

Collapse
 
morinoko profile image
Felice Forby

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

Collapse
 
angelica_karen profile image
Angelica Pinili • Edited

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)

Collapse
 
safventure11000 profile image
Josafe Balili • Edited

I'm currently learning ruby on rails and this post is really helpful. Thank you so much. I have a question, where is the best place to put the js link tag?

Currently, I put it in application.html.erb
<%= javascript_include_tag "cdn.ckeditor.com/4.16.1/full/ckedi..." %>
But I think there's still a better place to put it

Thanks in advance. :)

Collapse
 
jaemon profile image
Jonathan Owah

Method 3 worked perfectly for me. Thanks.

Collapse
 
scz profile image
Stef Coetzee

Thanks for the succinct article, Felice! I used Method 3 for some simple menu show/hide functionality.

Collapse
 
trillvinzini profile image
Jeff Fleury

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