How to write Javascript in Rails 6 | Webpacker, Yarn and Sprockets

Younes Serraj
Younes SerrajDec 13, 2019
Webpacker

Webpacker

Do you feel lost with all the changes related to assets and Javascript? Npm, Babel, ES6, Yarn, Webpack, Webpacker, Sprockets, do they all look like complete strangers to you?

If you need a quick, easy to understand topo on how this whole Javascript ecosystem works in a Rails 6 application, this article is what you’re looking for.

I’ll end up this article with a step-by-step section explaining how to add Bootstrap 4 and FontAwesome 5 to a Rails 6 project.


NPM

NPM is a Javascript package manager (NodeJS modules to be precise). It is the Rubygems of Javascript world.

npm install <package>

If you want to install bootstrap for instance:

npm install bootstrap

NPM stores downloaded packages in ./node_modules and keeps a list of these packages in ./package.json.

At this point, I’m not drawing any link between NPM and Rails, keep reading to understand why.


Yarn

Yarn is a more recent package manager for Javascript. It fetches packages from the NPM repository but does more than that. It allows you to lock the desired versions of your NPM packages in a yarn.lock autogenerated file (similar to Gemfile.lock), it is much faster than NPM, etc.

In a Rails 6 application, when you need a Javascript library, you:

  • used to add a gem that provides it, then you required it in app/assets/application.js (which was compiled by Sprockets)

  • have now to add it through Yarn (): yarn add <package>, then you require it (we'll see how later).

Note: NPM has since added a lock feature too through package-lock.json


ES6

ES6 is a new Javascript standard (a new version of Javascript if you will). It comes with new super-handy features such as class definition, destructuring, arrow functions, etc.

Goodbye Coffeescript, I always hated you.


Babel

Since all Web browsers don’t understand ES6 yet, you need a tool that reads your ES6 Javascript code and translates it into old ES5 Javascript for it to work on all browsers. Babel is the compiler that does this translation.


Webpack

There is Babel and there is Yarn and their configuration files and there is the need to automate the compilation of your assets and the management of environments and so on.

Because you want to focus on writing code and automate assets precompilation, you will be using Webpack which takes the role of bandmaster. It takes your assets and passes each one of them to the right plugins. The plugins then make the right tool process the input file and give the expected output.

For instance, Webpack can:

  • take your ES6 Javascript code,

  • use the

     

    babel-loader

     

    plugin to make Babel compile ES6 into ES5 Javascript code,

  • then output the resulting pack in a file that you can include in your HTML DOM (

    <script type="text/javascript" src="path-to-es5-javascript-pack.js"></script>).


Webpacker

Webpacker is a gem that nicely includes Webpack in your Rails application. It comes with some initial (and sufficient to begin with) configuration files so that you can start by writing actual code without worrying about configuration.

Webpacker’s default configuration says the following:

  • app/javascript/packs/ shall contain your Javascript packs (for instance: application.js)

  • You can include a Javascript pack in your views using javascript_pack_tag '<pack_name>' (for instance: <%= javascript_pack_tag 'my_app' %> will include app/javascript/packs/my_app.js)

I’ll give a very clear example of how all of this works at the end of this article, I just need to talk a bit about Sprockets first.

Note: another default configuration is extract_css: false (config/webpacker.yml), which means that although Webpack knows how to serve CSS packs with stylesheet_pack_tag, you tell it not to. This article is focused on Javascript so I'm not going to say more about this, just keep in mind that it is turned off by default so you won't waste time debugging what is default behavior, not a bug.

Yet another note: when you run rails assets:precompile, you might think that Rails will only precompile what’s in app/assets/. Rails will actually precompile both Webpack app/javascript/ assets and Sprockets app/assets/ assets.


Sprockets 4

Like Webpack, Sprockets is an asset pipeline, which means it takes assets files as input (Javascript, CSS, images, etc.) and processes them to produce an output in the desired format.

As of Rails 6, Webpack(er) replaces Sprockets as the new standard for writing Javascript in your Rails applications. However, Sprockets is still the default way of adding CSS to your applications.

With Sprockets you:

  • used to list available assets in config.assets.precompile (Sprockets 3, Rails 5)

  • have now to do that in a manifest file app/assets/config/manifest.js (Sprockets 4, Rails 6)

If you want to include an asset from the Sprockets pipeline, you would:

  • Write your CSS (for instance: app/assets/stylesheets/my_makeup.css)

  • Ensure app/assets/config/manifest.js makes it available for stylesheet_link_tag

    either through a link_tree, link_directory or a link statement (for instance: link my_makeup.css)

  • Include it in your view using stylesheet_link_tag (<%= stylesheet_link_tag 'my_makeup' %>)


Do not try to use Webpack as you would use Sprockets!

It is essential that you understand the following if you don’t want to waste countless hours rowing against the current. Ideally you should spend some time learning ES6 but meanwhile I can at least say this:

Webpack is different than Sprockets in the sense that it compiles modules.

ES6 modules to be precise (in the case of Rails 6 with a default configuration). What does that imply? Well, it implies that everything you declare in a module is kind of namespaced because it is not meant to be accessible from the global scope but rather imported then used. Let me give you an example.

You can do the following with Sprockets:

app/assets/javascripts/hello.js:

function hello(name) { console.log("Hello " + name + "!");}

app/assets/javascripts/user_greeting.js:

function greet_user(last_name, first_name) { hello(last_name + " " + first_name);}

app/views/my_controller/index.html.erb:

<%= javascript_link_tag 'hello' %>
<%= javascript_link_tag 'user_greeting' %><button onclick="greet_user('Dire', 'Straits')">Hey!</button>

Pretty simple to understand. How about with Webpacker now?

If you thought you’d simply move these JS files under app/javascript/packs, include them using javascript_pack_tag and be done, let me stop you right there: it would not work.

Why? Because hello() would be compiled as being in a ES6 module (likewise for user_greeting()), which means that as far as user_greeting() function goes, even after both JS files are included in the view, the hello() function does not exist.

So how would you get the same result with Webpack:

app/javascript/packs/hello.js:

export function hello(name) { console.log("Hello " + name + "!");}

app/javascript/packs/user_greeting.js:

import { hello } from './hello';function greet_user(last_name, first_name) { hello(last_name + " " + first_name);}

app/views/my_controller/index.html.erb:

<%= javascript_pack_tag 'user_greeting' %><button onclick="greet_user('Dire', 'Straits')">Hey!</button>

Would that work? No. Why? Again, for the same reason: greet_user is not accessible from the view because it is hidden inside a module once it is compiled.

We finally reach the most important point of this section:

  • With Sprockets: views can interact with what your JS files expose (use a variable, call a function, ..)

  • With Webpack: views do NOT have access to what your JS packs contain.

So how would you make a button trigger a JS action? From a pack, you add a behavior to an HTML element. You can do that using vanilla JS, JQuery, StimulusJS, you name it.

Here’s an example using JQuery:

import $ from 'jquery';
import { hello } from './hello';function greet_user(last_name, first_name) {
hello(last_name + " " + first_name);
}$(document).ready(function() {
$('button#greet-user-button').on(
'click',
function() {
greet_user('Dire', 'Strait');
}
);
});/* Or the ES6 version for this: */
$(() =>
$('button#greet-user-button').on('click', () => greet_user('Dire', 'Strait'))
);

app/views/my_controller/index.html.erb:

<%= javascript_pack_tag 'user_greeting' %><button id="greet-user-button">Hey!</button>

Rule of thumb: with Webpack, you setup the desired behavior in the packs, not in the views.

Let me repeat myself with one last example:

If you need to use a library (select2 or jQuery for instance), would it work to import it within a pack and use it in a view? No. You either import it in a pack and use it in that pack, or you read the next section of this article.

If you want to learn how to use StimulusJS to structure your JS code and attach behaviors to your HTML elements, I advise you read StimulusJS on Rails 101.

For those who want to understand how this “everything is hidden/namespaced” works: when an ES6 module is compiled into ES5 code, the module’s content is packaged inside an anonymous function so that outside of this function, you cannot access any variable/function declared in the module.


You still can use Sprockets for Javascript code

Webpacker’s documentation states the following:

[…] the primary purpose for webpack is app-like JavaScript, not images, CSS, or even JavaScript Sprinkles (that all continues to live in app/assets).

It means that if you need or want to make some Javascript stuff available to the views, you still can using Sprockets.

  • Create the app/assets/javascripts directory (notice javascripts here is in the plural form)

  • Update app/assets/config/manifest.js accordingly (//= link_directory ../javascripts .js)

  • Include your Sprockets Javascript files in your views using javascript_include_tag

    (notice the difference: javascript_include_tag for Sprockets, javascript_pack_tag for Webpacker)

  • Do your thing.

I personally try to avoid this as much as possible, but it is worth knowing.

Note : you might ask why there are both a manifest.js file and a config.assets.precompile array that serve the same purpose of exposing top-level targets to compile. This is for retro-compatibility purposes. The upgrading instructions discourage you to use the latter.


How to add bootstrap 4 and fontawesome 5 to a Rails 6 application

To help you understand better, I advise you apply as you read. It will help a great deal in apprehending these novelties.


1. Create a new Rails 6 application

rails new bloggy

I’d like you take a look at the following files. The goal is not for you to understand everything that they hold, just to know they exist and have a vague mental image of what they contain so that you can easily go back to them later if need be.

Yarn:

  • package.json

Webpacker:

  • config/webpacker.yml

  • app/javascript/packs/application.js

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

Sprockets:

  • app/assets/config/manifest.json


2. Add a root page

rails generate controller welcome index

And add root to: 'welcome#index' in config/routes.rb.

Run rails server and ensure everything is good so far.


3. Add required yarn packages

We want to add bootstrap 4 (which requires jquery and popper.js) and font-awesome 5.

Take a quick look at Yarn’s search engine and try to find the required packages on your own (notice the number of downloads for each package), then proceed with this tutorial.

yarn add bootstrap jquery popper.js @fortawesome/fontawesome-free

Yarn has now cached them in ./bloggy/node_modules/ and updated package.json. However these packages are still not used in our application. Let's fix that. We'll start by including the JS part first and will take care of the CSS part later.


4. Include the JS part of bootstrap and fontawesome

In your layout, there already is javascript_pack_tag 'application' which means you're asking Webpack to compile app/javascript/packs/application.js and include the output in this layout. To add bootstrap, we can either create another pack exclusively for including bootstrap or we can use the application.js pack. Let's do the latter as we're not building a real app.

Append the following to app/javascript/packs/application.js:

require("bootstrap");
require("@fortawesome/fontawesome-free");

Notice I required “bootstrap”, not “bootstrap/dist/js/bootstrap.min”. This is because unless I specify a file’s path, the module’s package.json (bloggy/node_modules/bootstrap/package.json) will give the necessary information on which file to include. I could've required "bootstrap/dist/js/bootstrap.min" and it would've worked just fine.

Back to setting up bootstrap and fontawesome in our application. If you start your Rails server and look at the Javascript console, you’ll see that it works correctly even though we did not require jQuery in application.js.

If you previously looked at other tutorials explaining how to include bootstrap through Webpacker, you probably noticed that most of them require jQuery first then require bootstrap. This is actually useless.

Why? Because since we installed jQuery through Yarn, bootstrap can on its own require jQuery. There’s no point for us to require it in application.js because it would make it available in application.js, not inside bootstrap module. So unless you actually need to use jQuery directly in application.js, there’s no need to require it there.


5. Include the (S)CSS part of bootstrap and fontawesome

I like to work with SCSS, so prior to including bootstrap and font-awesome, let’s rename application.css into application.scss and empty it from all comments and other Sprockets instructions.

Now past the following code into it:

$fa-font-path: '@fortawesome/fontawesome-free/webfonts';
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/regular';
@import '@fortawesome/fontawesome-free/scss/solid';
@import '@fortawesome/fontawesome-free/scss/brands';@import 'bootstrap/scss/bootstrap';

Note: Unlike Webpack, Sprockets won’t read npm modules’ package.json files to determine which file to include, therefor you can’t import a module just by its name. You have to specify the path to the actual file(s) that you wish to import (the file’s extension is optional, though).

You’re all set!

Let’s add a button and an icon in our view to make sure everything works properly:

Add <a href="#" class="btn btn-primary">Yeah <i class="far fa-thumbs-up"></i></a> to app/views/welcome/index.html.erb, run your rails server and ensure both the primary button and the icon appear correctly.


Make jQuery available in all packs

If you need to use jQuery (or any dependency) in most of your packs, requiring it in each pack is cumbersome. A solution that I like is to make it available for all packs through configuration (again, it will not be available in views, just in packs).

To achieve this, copy/past the following in config/webpack/environment.js:

const { environment } = require('@rails/webpacker')
var webpack = require('webpack');environment.plugins.append(
'Provide',
new webpack.ProvidePlugin({
$: 'jquery',
})
)module.exports = environment

This snippet makes Webpack “provide” the jQuery module to all packs through the name $. It is equivalent to adding the following at the beginning of each pack:

import $ from 'jquery';

Thanks for reading!

Partager
Younes Serraj
Younes SerrajDec 13, 2019

Capsens' blog

Capsens is an agency specialized in the development of fintech solutions. We love startups, scrum methodology, Ruby and React.