Organizing Javascript in Rails Application With Turbolinks

It’s impossible to escape Javascript in a Rails application. From a tiny script to a full-on Javascript framework, websites are becoming more and more reliant on Javascript, whether we like it or not.

Several articles back, I documented how I handle page-specific Javascript in a Rails application. My solution included a third-party jQuery plugin that did some magic on the $(document).ready function in combination with CSS style scoping to limit the functionality.

The plugin worked well for awhile, but with the advent of Turbolinks, the solution felt less and less appropriate. I’ve since settled on some techniques to not only handle page-specific Javascript, but overall organization and structure of Javascript within a Rails application. I’ve used it in a hand full of large applications over the past few months and it’s held up incredibly well.

The Problem

Using “sprinkles” of Javascript throughout a Rails application can get unwieldly fast if we’re not consistent. What we ideally want is some techniques and guidelines that can keep the Javascript organized in our projects. We also don’t want to have to disable Turbolinks to make our application work as we expect.

The Solution

Generally, javascript behavior can be boiled down to the following categories:

  1. Behavior that’s “always on”
  2. Behavior that’s triggered from a user action

But first, a few things to will help us stay organized…

Class Scoping

I still like to scope the body element of the layout(s) with the controller and action name:

<body class="<%= controller_name %> <%= action_name %>">
  <%= yield %>
</body>

This not only let’s us control access to the DOM through jQuery if we need to, but also provides some top-level styling classes to allow us to easily add page-specific CSS.

In the case we’re working on the proverbial blog posts application, the body tag ends up looking like:

<body class="posts index">
  <%= yield %>
</body>

This gives us the opportunity to scope CSS and Javascripts to all posts-related pages in the controller with the .posts class, or down to the specific page using a combination of both the controller and action: .posts.index.

Default Application Manifest

Here’s the default app/assets/javascripts/application.js:

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .

I start by removing the line //= require_tree .. I do this because if you don’t, the javascript files in the folder will be loaded in alphabetical order. As you’ll see below, there’s an initialization file that needs to be loaded before other Javascript. We’ll also remove the comments from the top of the file to preserve space.

So we’re left with:

//= require jquery
//= require jquery_ujs
//= require turbolinks

Initialization

Let’s start by adding the file app/assets/javascripts/init.coffee with the following:

window.App ||= {}

App.init = ->
  $("a, span, i, div").tooltip()

$(document).on "turbolinks:load", ->
  App.init()

Let’s dig in to each pagef of this:

window.App ||= {}

We’re creating the App object on window so the functionality added to the object is available throughout the application.

Next, we define an init() function on App to initialize common jQuery plugins and other Javascript libraries:

App.init = ->
  $("a, span, i, div").tooltip()

The call to $("a, span, i, div").tooltip() initializes Bootstrap Tooltips. This is an example of the type of libraries that can/should be setup here. Obviously, if you’re not using Bootstrap tooltips, you would haven’t this here, but coupled with the next line, we’ll see why this works.

As many have found out the hard way, when Turbolinks is enabled in a project, jQuery $(document).ready functions don’t get fired from page to page. In order to call the init() function on each page transition, we’ll hook in to the turbolinks:load event:

$(document).on "turbolinks:load", ->
  App.init()

Note: the turbolinks:load transition is also triggered on the well known document ready event, so there’s no need to add any special handling for first page load.

Lastly, we need to add init.coffee to the asset pipeline:

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require init

“Always On” Javascript Functionality

Now with the defaults out of the way, let’s take a look at adding some behavior.

Let’s assume one of our pages will show a Javascript graph of data. We’ll start by adding a file with a name related to that responsibility.

# app/assets/javascripts/app.chart.coffee

class App.Chart
  constructor: (@el) ->
    # intialize some stuff

  render: ->
    # do some stuff

$(document).on "turbolinks:load", ->
  chart = new App.Chart $("#chart")
  chart.render()

A few things to note here…

Structure

I created a class in the App namespace – the same we initialized in app/assets/javascripts/init.coffee. This gives us an isolated class that has a clear responsiblity. Like our Ruby, we want to do our best to keep its responsibilities to a minimium.

You might notice the file takes the form:

|
|
class definition
|
|


|
invocation
|

While this may seem obvious, it’s an important point to keep in mind. I’ve found it offers a predictable structure that allows me to open any coffeescript file that we’ve written in the project and generally know where to look for what.

Turbolinks-Proof

We called this “Always On” functionality because, as you probably noticed, using the following event listener $(document).on "turbolinks:load", ->, we know with Turbolinks, this gets triggered on every page transition.

Add to Manifest

Because we removed the //= require_tree . line in the default application.js manifest file, we’ll have to add our chart file to be included in the asset pipeline (last line):

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require init
//= require app.chart

Page-Specific Javascript

Uh oh, so maybe we don’t want the graph to show up on every page! In this case, we’re looking for “Always On” functionality for specific pages ONLY.

We can limit the page pages certain functionality is loaded on by using the classes we added to the body of the layout. In this case, a small conditional to the invocation can prevent this being triggered on pages it shouldn’t be.

$(document).on "turbolinks:load", ->
  return unless $(".posts.index").length > 0
  f = new App.Chart $("#chart")
  f.render()

We added return unless $(".posts.index").length > 0 to make sure App.Chart never gets instantiated if we’re on the .posts.index page. While this may seem verbose, I’ve found that it’s not very common to need page-specific functionality. There are probably plenty of libraries that do something similar, like the one I previously suggested. However, to me, limiting javascript to a single page and very explicit when I read the code, it’s almost never worth dragging in a separate plugin for this. YMMV.

User-Triggered Javascript

This type of Javascript is exactly what you’d think – Javascript invoked as a result of a user clicking or performing some type of action. You’re probably thinking, “I know how to do this, I’ll just add a random file to the javascripts directory and throw in some jQuery”. While this will functionally work just fine, I’ve found that keeping the structure of these files similar will give you great piece of mind going forward.

“data-behavior” Attribute

Let’s assume there’s a link in the user’s account that allows them to update their credit card. In this case, we have the following:

<%= link_to "Update Credit Card", "#", data: { behavior: "update-credit-card" } %>

You’ll probably notice the data-behavior tag being added to the link. This is the key we’ll use to attach the Javascript behavior.

We could have added a unique class to the link:

<%= link_to "Update Credit Card", "#", class: "update-credit-card" %>

or, perhaps, even assign an ID:

<%= link_to "Update Credit Card", "#", id: "update-credit-card" %>

Both of these techniques don’t really indicate whether we added the update-card-card for the use of CSS styling, or to attach Javascript behavior. So in my applications, I leave classes for styling ONLY.

So now to the Javascript:

App.Billing =
  update: ->
    # do some stuff

$(document).on "click", "[data-behavior~=update-credit-card]", =>
  App.Billing.update()

We can use the selector [data-behavior~=update-credit-card] to latch on to the data-behavior tag we defined in the view. We use the on jQuery method to ensure that we’re listening to this event whether the element’s on the page or not. This is what allows us to load this Javascript when on other pages and have it still work when a user clicks through to the page with the actual link on it.

We could latch on to the change event, or whatever is appropriate to the element we’re adding behavior.

Add to Manifest

Again, because Javascripts assets we add to app/assets/javascripts won’t automatically be inserted in to the asset pipeline, we’ll add //= require app.billing to the manifest file:

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require init
//= require app.chart
//= require app.billing

Summary

Using the techniques above, we can keep the Javascript in our Rails applications organized and predictable. We can rest easy knowing the files will all generally look the same. There’s not been any uses cases where this structure hasn’t worked for me personally.

One thing that makes me feel good about this approach is there’s no real magic or extra plugins. It’s using all the tools we already have in a basic Rails application, which is one less thing to maintain and keep up to date. Less depedencies == less pain down the road.