JavaScript

Style

Organization

Our JavaScript organization relies on two concepts:

Single entry point

The Single entry point handles how the JavaScript bootstraps the client side code based on the current page in the browser, which helps to organize application in a way we can relate pieces of the client side code with the HTML code.

Page handlers draw a parallel to Rails controllers and actions, and the Single entry point implementation mimics the Front Controller pattern, implemented in most web frameworks.

Using page.js, for instance, we can have page specific handlers that are responsible for instantiating any component required by the interface or firing any necessary setup code.

// app/assets/javascripts/pages/orders/index.js
// The following function executes when we render the `orders/index.html.erb`
// view, which adds a `data-page="orders#index"` attribute to the `body`
// element.
page.at('orders#index', function() {
  $('[data-chosen]').chosen();
});

With this approach we can isolate the code execution and event binding from the definition of the components or plugins used in the project, which helps developers to navigate through the code base and understand the execution flow and how the rest of the JavaScript code relates to the pages and sections of the application.

Page handlers, as server side controllers, shouldn’t contain application logic and be responsible for orchestrating the execution of other components, and injecting any dependencies (like specific DOM elements) or binding elements to help one component communicate with another.

// app/assets/javascripts/pages/projects/index.js
// The handler injects the required DOM elements for each of the components
// used in the interface, binds one with another through an event handler
// and spins up the `projectSelector` events.
page.at('projects#index', function() {
  var projectSelector = new ProjectSelector('[data-projects]'),
      chart = new ActivityChart('[data-chart]');

  projectSelector.on('project:selected', function(project) {
    chart.render(project);
  });
  projectSelector.bindEvents();
});

Script Loaders

For delaying the JavaScript execution and state the dependencies between different components and modules we recommend the usage of the [Asynchronous module definition] (https://en.wikipedia.org/wiki/Asynchronous_module_definition) paired with [almond] (https://github.com/jrburke/almond) for loading the modules after the Asset Pipeline concats everything into a bundled file.

The AMD format wraps each entity into a define function call, with a module ID identifier, an Array of dependencies and a factory function, and provides a require function to require modules elsewhere in the app.

// app/assets/javascripts/components/plans-selection.js
define('plans-selection', [], function() {
  function PlansSelection(element) { };

  // PlansSelection implementation...

  return PlansSelection;
});

// app/assets/javascripts/pages/accounts/new.js
// require components/plans-selection

page.at('accounts#new', function() {
  var PlansSelector = require('plans-selection');
  var selection = new PlansSelection('[data-plans-selection]');
  // ...
});
ES6 Modules

For projects using the ES6 with the Babel transpiler, we recommend the usage of the ES 6 Modules syntax with almond as a Loader polyfill.

# config/initializers/assets.rb
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
// app/assets/javascripts/components/plans-selection.es6
export default class PlansSelection {
  // PlansSelection implementation...
}

// app/assets/javascripts/pages/accounts/new.es6
// require components/plans-selection
import PlansSelection from 'components/plans-selection';

page.at('accounts#new', function() {
  var selection = new PlansSelection('[data-plans-selection]');
    // ...
});
Third party dependencies

For any third party dependency that is commonly used as a global variable (like jQuery and Turbolinks) or isn’t well suited for the loading pattern used by the application, we should use that library as global dependency instead of shoehorn it into the module format and risk introducing any API inconsistency or update issue into the app.

// LoDash does not exports a named module and doesn't work out of the box
// with almond, so we require it upfront and rely on the global `_` variable.
//= require lodash
//= require almond

Directory structure

We suggest the following directory for Rails and non Rails projects when we aren’t using a client side framework like Ember that bring its own patterns to the app.

Is not required to create all directories upfront, and the project can adopt this structure as the codebase grows and requires more abstractions. You can start with a simple application.js monolith and go from there as necessary.

# Your JavaScript root directory: `app/assets/javascripts` for Rails projects,
# or 'javascripts' elsewhere.
├── application.js # Main bundle
├── components
│   └── # Abstractions for interface elements bound to the DOM and are responsible
        # for event binding and the desired user interaction by the app.
├── pages
│   └── # Page handlers for the single entry point architecture.
├── modules
│   └── # Abstractions that don't fit the use case of interface elements, like
        # view models, patterns and service implementations.
├── support
│   └── # Extra files or configurations, like polyfills and small extensions of
        # general use.
└── vendor
    └── # 3rd party libraries and dependencies, alternative for `vendor/assets`
        # for non Rails projects.

ESLint

It is strongly advised to use ESLint to detect formatting errors and potential problems with your JavaScript code. You can install it with npm install -g eslint so the eslint command will be available through your system. You can then setup your editor to analyze your code automatically in development with any of the following tools:

You should add a .eslintrc file to the root of your project directory to customize how ESlint will inspect your code and which type of warnings you want to look for. You can start with something like the following settings:

{
  "env": {
    "browser": true,
    "es6": true
  },
  "globals": {
    "$": true
  },
  "ecmaFeatures": {
    "modules": true
  },
  "rules": {
    "quotes": [2, "single"],
    "comma-dangle": [2, "never"],
    "indent": [2, 2, { "VariableDeclarator": 2 }],
    "camelcase": 2,
    "no-multi-spaces": 0,
    "no-shadow": 2,
    "no-eq-null": 2,
    "no-extra-parens": 2,
    "no-lonely-if": 2,
    "no-nested-ternary": 2,
    "no-param-reassign": 2,
    "no-self-compare": 2,
    "no-throw-literal": 2,
    "no-void": 2,
    "no-undef": 2,
    "no-extra-semi": 2,
    "no-underscore-dangle": 0,
    "eqeqeq": 2,
    "comma-style": [2, "last"]
  }
}

To know more about all the settings available, please refer to the official documentation.