A Guide to Custom and Third-Party Javascript with Rails 6, Webpacker and Turbolinks

Ihave recently had the pleasure of updating our app from Rails 4 to Rails 6. Though it initially seemed to be a very smooth transition, I came across some minor problems as testing got underway.

The biggest issue was, of course, that NONE of my custom Javascript modules worked and I was encountering bugs with my Bootstrap and SB-Admin-2 JS scripts.

Some of the bugs I was seeing in my browser developer console were:

TypeError: $(…).metisMenu is not a function

Uncaught ReferenceError: $ is not defined

And absolutely nothing online was (a) relevant to my stack and (b) solving my problem. So for this reason, I thought I’d put up something in case other people run into the same problem.

NB: Other solutions online may work for those who do not implement Turbolinks in their stack.

Webpacker Set-Up

As this guide is not for beginners, I will not be giving an introduction to Webpacker (if needed, see: Guide 1, Guide 2 and Guide 3). I found the directory structure below most suitable for my needs:

When you run rails webpack:install, the javascript folder is automatically created, complete with the packs folder and the application.js and application.scss files. Taking this structure, I added another folder, custom, which I used to store my custom and third-party modules.

My application.scss file is empty for the moment and the contents of my application.js are:

console.log(‘Hello from application.js’)

(Keep in mind that if you copy the above code directly, you will get errors as the quotation marks are not recognised!!!)

My custom modal module pertains to the functionality of my modals. I wanted their contents to be changed depending on the link that was used to show them using data-target parameters. For reference I provided my full modal.js code at the bottom of the article.

Below are the packages found in package.json, compare the list to yours and determine if you have any missing.

"dependencies": {
"@fortawesome/fontawesome-free": "^5.14.0",
"@rails/activestorage": "^6.0.3-2",
"@rails/ujs": "^6.0.3-2",
"@rails/webpacker": "5.1.1",
"autoclean": "^1.0.2",
"bootstrap": "^4.5.0",
"channels": "^0.0.4",
"core-js": "^3.6.5",
"expose-loader": "^1.0.0",
"fontawesome": "^5.6.3",
"jquery": "^3.5.1",
"jquery-ujs": "^1.2.2",
"metismenu": "^3.0.6",
"popper": "^1.0.1",
"popper.js": "^1.16.1",
"rails-ujs": "^5.2.4-3",
"regenerator-runtime": "^0.13.7",
"startbootstrap-sb-admin-2": "^4.1.1",
"turbolinks": "^5.2.0",
"yarn": "^1.22.4"
"devDependencies": {
"webpack-dev-server": "^3.11.0"

And finally, in your app/config/webpack/environment.js, you want the following:

const { environment } = require('@rails/webpacker');
const webpack = require("webpack");
environment.plugins.append("Provide", new webpack.ProvidePlugin({
$: 'jquery',
jquery: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
Popper: ['popper.js', 'default']
module.exports = environment;

That’s all for the Webpacker. Moving on to the Javascript…

Custom and Third Party Javascript

The problem with running the custom modules alongside Turbolinks is that you are no longer waiting for the ‘document’ to load, but for Turbolinks to initialise the page. So while your code may have previously looked like this:

function readyFn(jQuery) {
console.log("Hello World!")
$(window).on('load', readyFn);

It will now have to look like this:

function readyFn(jQuery) {
console.log("Hello World!");
$(document).on('turbolinks:load', readyFn);// or $(document).on('turbolinks:load', function() {
function() {
console.log("Hello World!");

I added the $(document).on('turbolinks:load', function() { //code } to the top of the SB-Admin-2.js file so that it was also comaptible with Turbolinks. And that’s it! I hope this was helpful and will save someone out there days of tearing their hair out and going for angry runs.


$(document).on('turbolinks:load', function() {
$('#preview').on('shown.bs.modal',function (event) {

var link = $(event.relatedTarget);
var type = link.data('type');
var id = link.data('linkid');
var modal = $(this); if (type.includes("video")) {
modal.find('.modal-body video').attr('src', id);
modal.find('.modal-body video').attr('id', 'modal-activated');
modal.find('.modal-body video').attr('data-type', type);
modal.find('.modal-body video').toggleClass('d-none');
else if (type.includes("image")) {
modal.find('.modal-body img').attr('src', id);
modal.find('.modal-body img').attr('id', 'modal-activated');
modal.find('.modal-body img').attr('data-type', type);
modal.find('.modal-body img').toggleClass('d-none');
});$(document).on('turbolinks:load', function () {
$('#preview').on('hidden.bs.modal', function (event) {
var link = $('#modal-activated');
var type = link.data('type');
var modal = $(this);if (type.includes("video")) {
modal.find('.modal-body video').toggleClass('d-none');
else if (type.includes("image")) {
modal.find('.modal-body img').toggleClass('d-none');

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store