Using ES2015 with Asset Pipeline on Ruby on Rails
Read in 6 minutes
JavaScript is all new. Until recently, we had no new features. The last significant update was back in 2009, with ES5‘s release. And you couldn’t use all features due to browser incompatibility.
To increase the compatibility level, we had to use things like es5-shim, which conditionally checked if a feature was available, adding a polyfill if the browser didn’t implement it.
And then the first pre-processors came in, like CoffeeScript. You could write different constructions, that were compiled to code that the browsers could actually understand.
Interestingly, Brendan Eich announced in 2009 a new JavaScript version called Harmony, which is now called ES20151. The first drafts were published in 2011, but the final specification was released in June of 2015.
ES2015 has so many new features:
- Class definition
- String interpolation
- Fat arrow functions
- More!
Browsers are implementing ES2015 in a fast pace, but it’ll take some time until we can actually use it. Maybe years. Fortunately, we have Babel.js. We can use all these new features today without worrying with browser compatibility.
Babel.js2 is just a pre-processor. You write code that uses these new features (and more), which will be exported as code that browsers can understand, even those that don’t fully understand ES2015.
Using Babel.js with Asset Pipeline
There are several ways you can use to precompile your JavaScript code. Some people use Babel’s CLI. Some people prefer builders like Grunt or Gulp to automate the compilation process. But if you’re using Ruby on Rails you’re more likely to use Asset Pipeline for front-end assets compilation. And this is, in my opinion, the easiest way of using Babel, believe it or not.
Unfortunately there’s no built-in support on the stable release of Sprockets, so you’ll have to use a pre-release version.
The transpilation is performed by babel-schmooze-sprockets. It vendors several Babel extensions so that you can use Babel without having to deal with NPM on your application (you’ll still need Node.js though).
Update your Gemfile
to include these dependencies.
source "https://rubygems.org"
gem "rails", "4.2.6"
gem "sqlite3"
gem "uglifier", ">= 1.3.0"
gem "sprockets", "~> 4.x"
gem "babel-schmooze-sprockets"
gem "turbolinks", "~> 5.x"
gem "jquery-rails"
That’s it! Now all .es6
files will be compiled using Babel. Create a file at app/assets/javascripts/hello.es6
with the following code:
class Hello {
constructor() {
alert("Hello!");
}
}
new Hello();
Make sure you’re loading hello.es6
at app/assets/javascripts/application.js
. You also need to load Babel helpers; they’ll be used to reduce the amount of generated code.
//= require babel
//= require_tree .
//= require_self
Now if you access the page on your browser, you’ll see an alert
box like this:
If you want the new module system, you still have some things to configure.
Using ES2015 modules
ES2015 introduced module support; instead of defining your code in the global scope, you can use a scope per file. Importing modules is pretty straightforward:
import Foo from "foo";
This will load Foo
from foo.js
. One single file can export several units (functions, objects, anything you want), and you don’t have to load everything at once, pretty much like Python.
Babel has no idea of how these modules should be exported, and by default, will use the CommonJS format, which can’t be used by the browser, so we’ll use another approach.
The easiest way is using AMD, and for this we’ll use almond. You can install almond with whatever you’re using for managing packages; in this article I’ll use http://rails-assets.org, a bower-to-rubygems converter. Update your Gemfile
like the following:
source "https://rubygems.org"
gem "rails", "4.2.6"
gem "sqlite3"
gem "uglifier", ">= 1.3.0"
gem "sprockets", "~> 4.x"
gem "babel-schmooze-sprockets"
gem "turbolinks", "~> 5.x"
gem "jquery-rails"
source "https://rails-assets.org" do
gem "rails-assets-almond"
end
Finally, update app/assets/javascripts/application.js
so it loads almond.
//= require turbolinks
//= require almond
//= require jquery
//= require_tree .
//= require_self
Notice that Turbolinks must be loaded before Almond; this happens because Turbolinks is an anonymous AMD module. For more information, check the section “Almond.js gotcha”, later on this article.
Now you have to think about the execution process. Are you going to use some code dispatcher? Or execute code based on the view that is being rendered? Are you going to create your own execution mechanism? The answer depends on your own workflow, so I’m not going to give you too many alternatives here.
We’re going to create a boot script that uses the controller and action names to execute the JavaScript you need for that specific page. Just add the require
call to your app/assets/javascripts/application.js
file.
//= require turbolinks
//= require almond
//= require jquery
//= require_tree .
//= require_self
require(["application/boot"]);
You have to create app/assets/javascripts/application/boot.es6
. I’ll listen to some events, like DOM’s ready
and Turbolinks’ turbolinks:load
.
import $ from "jquery";
function runner() {
var path = $("body").data("route");
// Load script for this page.
// We should use System.import, but it's not worth the trouble, so
// let's use almond's require instead.
try {
require([path], onload, null, true);
} catch (error) {
handleError(error);
}
}
function onload(mod) {
// Assign the default module.
var Page = mod.default;
// Instantiate the page, passing <body> as the root element.
var page = new Page($(document.body));
// Set up page and run scripts for it.
if (page.setup) {
page.setup();
}
page.run();
}
// Handles exception.
function handleError(error) {
if (error.message.match(/undefined missing/)) {
console.warn("missing module:", error.message.split(" ").pop());
} else {
throw error;
}
}
$(window)
.ready(runner)
.on("turbolinks:load", runner);
This script needs a data-route
property property on your <body>
element. You can add something like the following to your layout file (e.g. app/views/layouts/application.html.erb
). You can use the following helper method:
# app/helpers/application_helper.rb
module ApplicationHelper
ACTION_ALIASES = {
"update" => "edit",
"create" => "new"
}
def js_route
action_name = ACTION_ALIASES[controller.action_name] || controller.action_name
controller_name = controller.class.name.underscore.gsub("_controller", "")
"#{controller_name}/#{action_name}"
end
end
And then:
<body data-route="application/pages/<%= js_route %>">
Now let’s create a class that will be executed when the template is rendered. We’ll use the site
controller and home
action as example. For this you’ll need to create the app/assets/javascripts/application/pages/site/home.es6
file.
export default class Home {
constructor(root) {
this.root = root;
}
setup() {
// add event listeners
console.log("-> setting up listeners and whatnot");
}
run() {
// trigger initial action (e.g. perform http requests)
console.log("-> perform initial actions");
}
}
Note that we’re exporting the Home
class as the default module. This is the module that will be used when you have something like import Home from 'application/pages/home';
.
Also note that we’re defining the Home#constructor
method; this is the method that is executed when the class is instantiated.
I said this before, but class definition is one of the things I like the most. Compare it with the constructor function form:
function Home(root) {
this.root = root;
}
Home.prototype.setup = function() {
// add listeners
};
Home.prototype.run = function() {
// initial execution
};
They’re are similar, but using the class
keyword makes closer to what we use in other languages. Here’s how you would write the same class in Ruby:
class Home
def initialize(root)
@root = root
end
def setup
end
def run
end
end
ES2015 has other niceties. To discover what’s new, I recommend the Exploring ES6 book, which you can read for free.
Almond.js gotcha
There is one gotcha when using Almond; all modules must be explicitly named. This means that libraries like qunit won’t work out of the box because they’re anonymous modules. To solve this problem, you should load the library before loading Almond and then exporting the module yourself.
//= require qunit
//= require almond
define("qunit", function() {
return QUnit;
});
The problem of doing this is that the library won’t detect AMD support and will export global variables, but I still prefer this behavior over compiling code with an optimizer like r.js or a library that integrates that into Rails.
Wrapping up
Using ES2015 today is a viable option. With Babel you can use all these new features without worrying with browser compatibility. The integration with Asset Pipeline make things easier, even for those that don’t fully grasp the Node.js ecosystem.
There’s a working repository available at Github.
ES2015 is also known as ES.Next or ES6. ↩
This project used to be called 6to5.js. Read more. ↩