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:
- Behavior that’s “always on”
- 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:
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:
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
:
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:
Initialization
Let’s start by adding the file app/assets/javascripts/init.coffee
with the following:
Let’s dig in to each pagef of this:
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:
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:
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:
“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.
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:
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):
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.
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:
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:
or, perhaps, even assign an ID:
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:
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:
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.