Migrating FormKeep to ember-cli-rails

We’ve recently finished migrating FormKeep’s administrative dashboard from using the ember-rails gem to ember-cli-rails.

We chose to migrate to ember-cli-rails so that we could separate our client and server-side codebases and workflows, align our project’s conventions with the Ember.js core team and community, and utilize the rich EmberCLI addon ecosystem, while still running truly end-to-end JavaScript-enabled Capybara integration tests.

Our migration consisted of two main steps:

  1. Modify our files and directories to be ember-cli compliant,
  2. Integrate our ember-cli app with our Rails app.

Migrating to ember-cli

Background

Rails applications serving Sprockets-based Ember applications don’t enforce any strict conventions. The location of files is unimportant. Applications are wired together by global assignments to window.App, regardless of the source file’s name or location.

Although it’s discouraged, Sprockets-based applications are capable of declaring classes from any location, sometimes declaring multiple classes per file. There is nothing preventing app/assets/javascripts/router.js from defining App.Router along with App.PostsRoute and App.Post.

EmberCLI applications, on the other hand, use an ES6 module resolver to enforce that files are named properly and reside in their conventional location.

The examples in this post will follow the traditional file structure conventions. If you’d like the migrate to a pod-based project, the approach is similar but the directories are slightly different.

A set of rough guidelines

At a high level, migrations follow a general pattern:

  1. Move directories from app/assets/javascripts/$TYPE to app/$TYPE.
    • app/assets/javascripts/templates -> app/templates
  2. Replace _ with - in all filenames.
    • app/assets/javascripts/helpers/in_dollars.js -> app/helpers/in-dollars.js
  3. Remove _$TYPE suffixes.
    • app/assets/javascripts/views/posts_view.js -> app/views/posts.js
  4. Nest children under their parents.
    • app/assets/javascripts/views/posts_new_view.js -> app/views/posts/new.js

For a more in-depth guide, check out this post and the ember-cli documentation.

While these guidelines will work for most files, there are some additional quirks to be aware of.

Ember-Data

Sprockets-based Ember applications typically include an app/assets/javascripts/store.js file responsible for configuring and extending Adapters, Serializers, and Transforms.

This file commonly ends up becoming a junk drawer full of ember-data configurations.

EmberCLI expects these declarations to be located in app/adapters, app/serializers, and app/transforms, respectively.

Any calls to .reopenClass must be moved to their own initializer.

Initializers

Sprockets-based applications require initializers to be explicitly registered:

// app/assets/javascripts/initializers/foo_initializer.js

Ember.Application.initializer({
  name: "foo",
  initialize: function(container, application)  {
    /* initialize foo */
  }
});

In module based EmberCLI applications, the resolver will automatically load initializers based on the properties exported by files in app/initializers:

// app/initializers/foo.js

export default {
  name: "foo",
  initialize: function(container, application)  {
    /* initialize foo */
  }
};

Helpers

Sprockets-based applications require helper factories to be registered by Ember.Handlebars.helper:

// app/assets/javascripts/helpers/in_dollars_helper.js
Ember.Handlebars.helper("in-dollars", function(amount) {
  /* show in dollars */
});

EmberCLI’s resolver requires helpers to be created by Ember.Handlebars.makeBoundHelper and exported from their own module:

// app/helpers/in-dollars.js

import Ember from "ember";

export default Ember.Handlebars.makeBoundHelper("in-dollars", function(amount) {
 /* show in dollars */
});

Special attention must be paid to single-word helpers like pluralize. EmberCLI requires these helpers to be exported as functions, then imported and registered manually.

// helpers/pluralize.js
export default function(word, count) {
  /* pluralize word */
}
// app/app.js

import Ember from "ember";
import pluralizeHelper from "./helpers/pluralize";

Ember.Handlebars.registerBoundHelper("pluralize", pluralizeHelper);

When in doubt, refer to the documentation.

Design assets

FormKeep uses the majority of thoughtbot’s design tools, which require SCSS support.

Luckily, there is an Ember Addon for SCSS, Bourbon, and Neat:

$ ember install ember-cli-{bourbon,neat,sass}

When moving styles from app/assets/stylesheets to app/styles, keep in mind:

  • the asset pipeline helpers (such as image_url) are snake_cased, while the EmberCLI tools expect helpers to be kebab-cased (such as image-url).
  • Node’s SCSS compiler doesn’t support globbing, so calls to import directory/*, must be replaced with explicit calls to import directory/foo.

Tests

Before the migration, FormKeep tested its front end with a combination of end-to-end JavaScript-enabled Capybara-Webkit feature tests, and lower-level fine-grained QUnit BDD tests run with Teaspoon.

The feature test suite ensured that our front and back ends were wired together properly, while the JavaScript test suite ensured that our features behaved the way we intended.

Since our Teaspoon suite was already JavaScript, the migration was very straightforward:

  • move tests from spec/javascripts/integration to tests/acceptance
  • replace _ with - in filenames

For example, spec/javascripts/integration/user_edits_a_form_spec.js became tests/acceptance/user-edits-a-form-test.js

Teaspoon’s test suites rely on Sprockets’ require directive to concatenate auxiliary files onto the global namespace.

// spec/javascripts/support/date-helpers.js

window.yesterday = function() { /* get Yesterday's date */ };
window.today = function() { /* get Today's date */ };
// spec/javascript/spec_helper.js

//= require application
//= require app
//= require support/date-helpers

var App;

window.startApp = function() { /* start the App */ };
window.stopApp = function() { /* stop the App */};

before(function() {
  startApp();
});

after(function() {
  stopApp();
});
// spec/javascripts/integration/user_creates_form_spec.js

describe("The new form", function() {
  it("is created today", function() {
    // ...
    expect(form.get("createdAt")).to.equal(this.today());
  });
});

EmberCLI, on the other hand, avoids declaring values to the global namespace. Instead, the resolver relies on ES6 imports to act as an explicit declaration of a file’s dependencies. In order to invoke a helper method, we must first import it.

// tests/helpers/date.js

function yesterday() { /* get Yesterday's date */ }
function today() { /* get Today's date */ }

export {
  yesterday,
  today
};
// tests/helpers/acceptance.js

function startApp { /* start the App */ }
function stopApp { /* stop the App */}

export {
  startApp,
  stopApp
};
// tests/acceptance/user-creates-form-test.js

import { startApp, stopApp } from "../helpers/acceptance";
import { today } from "../helpers/date";

describe("The new form", function() {
  before(function() {
    startApp();
  });

  after(function() {
    stopApp();
  });

  it("is created today", function() {
    // ...
    expect(form.get("createdAt")).to.equal(today());
  });
});

Configuration

Before our migration, we had been declaring application-wide configuration values (sometimes from Rails’ ENV) into a <script> tag in our document’s <head>.

According to EmberCLI, these configuration values should be declared in the environments file.

Integrating with Rails

Once the code that powers our administrative dashboard was separated into its own EmberCLI app, we used ember-cli-rails to integrate with our Rails app.

Our configuration is very similar to the ember-cli-rails getting started guide:

# config/initializers/ember-cli.rb

EmberCLI.configure do |c|
  c.app(
    :frontend,
    build_timeout: 10,
    path: Rails.root.join("frontend"),
    enable: lambda do |path|
      # disable asset compilation during request specs
      !path.starts_with?("/api/")
    end
  )
end

Cross Site Request Forgery protection

Rails’ protect_from_forgery requires a CSRF token for every XHR except for GET. The CSRF token is normally found in app/views/layouts/application.html.* inserted with the rails helper: csrf_meta_tags.

@tricknotes

We opted to inject a valid <meta name="csrf-token"> into our document’s <head> before our server rendered the HTML.

On the client side, we added an initializer that introduces a jQuery ajaxPrefilter to add the header:

// app/initializers/jquery-csrf.js

export default {
  name: "csrf",
  initialize: function() {
    $.ajaxPrefilter(function(options, originalOptions, xhr) {
      var token = $('meta[name="csrf-token"]').attr("content");
      xhr.setRequestHeader("X-CSRF-Token", token);
    });
  }
};

Workflow

FormKeep’s workflow includes running the following in their own processes:

  • foreman start from the project root runs the API server and serves the EmberCLI application with ember-cli-rails (through the asset pipeline)
  • ember test --serve from app/frontend runs the QUnit tests against the front end

Depending upon what is being developed, each of these commands could be run in isolation.

To run both the Rails and Ember test suite, we declare a rake dependency between spec and ember:test:

# Rakefile

task default: ["spec", "ember:test"]
# run the full test suite
$ rake spec

Our outcome

We’re satisfied with where the migration has taken us:

  • Our project is aligned with the conventions of the core team and the community
  • We’re utilizing some awesome Ember addons that wouldn’t have been available through Sprockets
  • We’ve taken the first steps towards separating our API from our (one day static) client
  • Maintained the ability to run truly end-to-end integration tests

Further readings

When we started our migration, EmberCLI Migrator was in its infancy. It’s a more viable tool now, and definitely worth considering.