Ler blog em Português

Using ES6 with Asset Pipeline on Ruby on Rails

Read in 7 minutes

This article has been updated. Read Using ES2015 with Asset Pipeline on Ruby on Rails instead.

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 Harmony1, which is now called ES6. The first drafts were published in 2011, but the final specification was released in June of this year.

ES6 has so many new features:

Browsers are implementing new features in a fast pace, but it would still take some time until we could actually use ES6. 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, which will be exported as code that browsers can understand, even those that don’t fully understand ES6.

Using Babel.js

To install Babel make sure you have Node.js installed. Then you can install Babel using NPM.

$ npm install babel -g

You can now use babel command to compile your JavaScript files. You can enable the watch mode, which will automatically compile modified files. The following example with watch src and export files to dist.

$ babel --watch --out-dir=dist src

One of the features I like the most is the new class definition, which abstracts constructor functions. In the following example I create a User class that receives two arguments in the initialization.

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

After processing this file with Babel, this is what we have:

'use strict';

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

var User = (function () {
  function User() {
    _classCallCheck(this, User);
  }

  _createClass(User, [{
    key: 'construct',
    value: function construct(name, email) {
      this.name = name;
      this.email = email;
    }
  }]);

  return User;
})();

var user = new User('John', 'john@example.com');

console.log('name:', user.name);
console.log('email:', user.email);

Note that Babel generates all the required code for supporting the class definition. Alternatively, you can use --external-helpers to generate code that uses helpers (helpers can be generated with the babel-external-helpers command).

$ babel --external-helpers --watch --out-dir=dist src

Now all supporting code will use the babelHelpers object.

'use strict';

var User = (function () {
  function User() {
    babelHelpers.classCallCheck(this, User);
  }

  babelHelpers.createClass(User, [{
    key: 'construct',
    value: function construct(name, email) {
      this.name = name;
      this.email = email;
    }
  }]);
  return User;
})();

var user = new User('John', 'john@example.com');

console.log('name:', user.name);
console.log('email:', user.email);

Using Babel.js with Asset Pipeline

Instead of using 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.

Unfortunately there’s no built-in support on the stable release of Sprockets. But if you like to live on the edge3, you can use the master branch.

Update your Gemfile to include these dependencies.

source 'https://rubygems.org'

gem 'rails', '4.2.4'
gem 'sqlite3'
gem 'uglifier', '>= 1.3.0'

gem 'sass-rails', github: 'rails/sass-rails', branch: 'master'
gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
gem 'sprockets', github: 'rails/sprockets', branch: 'master'
gem 'babel-transpiler'

gem 'turbolinks'
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.

//= require_tree .
//= require_self

Now if you access the page on your browser, you’ll see an alert box like this:

Alert box - Hello

If you want the new module system, you still have some things to configure.

Using ES6 modules

ES6 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.4'
gem 'sqlite3'
gem 'uglifier', '>= 1.3.0'

gem 'sass-rails', github: 'rails/sass-rails', branch: 'master'
gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
gem 'sprockets', github: 'rails/sprockets', branch: 'master'
gem 'babel-transpiler'

gem 'turbolinks'
gem 'jquery-rails'

source 'https://rails-assets.org' do
  gem 'rails-assets-almond'
end

You also have to configure Babel; just create a file at config/initializers/babel.rb with the following code:

Rails.application.config.assets.configure do |env|
  babel = Sprockets::BabelProcessor.new(
    'modules'    => 'amd',
    'moduleIds'  => true
  )
  env.register_transformer 'application/ecmascript-6', 'application/javascript', babel
end

Finally, update app/assets/javascripts/application.js so it loads almond.

//= require almond
//= require jquery
//= require turbolinks
//= require_tree .
//= require_self

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 almond
//= require jquery
//= require turbolinks
//= 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’ page load.

import $ from 'jquery';

function runner() {
  // All scripts must live in app/assets/javascripts/application/pages/**/*.es6.
  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(Page) {
  // 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('page: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):

<body data-route="application/pages/<%= controller.controller_name %>/<%= controller.action_name %>">

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

ES6 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 ES6 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.


  1. ES6 is also known as ES.Next or ES2015 (this is the official name, defined after I first published this article). 

  2. This project used to be called 6to5.js. Read more. 

  3. The Go community does this all the time, so maybe is not a big deal. ¯\(ツ)/¯