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 includeapp/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 forstylesheet_link_tag
either through a
link_tree
,link_directory
or alink
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!