Creating page-specific JavaScript and CSS in Rails

written in css, front-end, javascript, rails

The Rails Asset Pipeline is a really great tool that improves performance by compressing and minifying assets, as well as compiling JS and CSS assets written in other languages, like CoffeeScript and SASS. When compiled, the files in your app/assets/javascripts/ folder get processed and minified into a single application.js file. In many cases, this is great - all of the views in your app have access to this file at all times, and it cuts down on the number of server requests made by the browser. But what happens if you want to dynamicaly scope JavaScript functions to specific pages in your app, like a function that only fires when an object view page is loaded?

I recently built a small Rails app as a part of an exercise in data analysis and visualisation. Using a set of On-Time performance data for flights in the United States, it visualised various performance criteria to help determine what carrier would be ideal for a New York-based company. As part of the challenge, I wanted to create maps visualising the metropolitan areas served by each carrier, and the relative population sizes of these areas. Here’s what the end product looked like:

Example bubble map of metropolitan areas

To render these maps, I was using the D3 JavaScript library. I needed to trigger two AJAX requests to my Carrier controller for two TopoJSON files (one for the US map, and one for the metropolitan areas). The latter of these two TopoJSON files was dynamically built in the carriers#data controller action and was unique to each instance of the Carrier model. I wanted these AJAX functions to fire as soon as the show page was loaded.

The typical way to trigger a JavaScript function upon page load would be via a Document Ready function, which fires once the DOM is loaded, as such:

1
2
3
$( document ).ready(function() {
    // Code goes here
});

However, as the Rails Asset Pipeline pre-compiles all your JavaScript and CoffeeScript files into a minified Application.js file, a Document Ready function will fire when any page in your app loads. One solution would have been to create a separate carrier.js file and include it manually in the Carrier show view like so:

1
<%= javascript_include_tag "carrier" %>

This would require a separate JS file to be loaded alongside the application.js file, and for me to add specific instructions to the production.rb file. This is okay, but it seemed like there should be a more elegant solution.

The jQuery ReadySelector plugin

Indeed, a more elegant solution exists: the jQuery ReadySelector plugin. This plugin extends the .ready() function for when a specific element of the DOM is loaded. In Rails, the suggested option is to dynamically generate class names for the <body> tags of your view matching the current controller action being used. Using ERB tags, you can achieve this with the following code:

1
<body class="<%= controller_name -%> <%= action_name -%>">

With this in place, all of my Carrier show pages were rendered inside of <body class="carriers show"> tags. In my carriers.js file, I included the following code that would trigger the relevant AJAX requests whenever a carrier show page was loaded:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$('.carriers.show').ready(function() {
  $.ajax({
    type: "GET",
    contentType: "application/json; charset=utf-8",
    url: 'data',
    data: {'id': $('#metro-map').data('params-id')},
    dataType: 'json',
    success: function (data) {
      $.getJSON('usData', function(usData){
        draw(usData, data);
      })
    },
      error: function (result) {
      error();
    }
  });
})

A few notes to explain what is happening. This function makes an AJAX request to the #data action in the Carrier controller, which serves back a topoJSON file. You’ll note it uses the ‘data’ field to pass the ID of the specific carrier to controller. I did this by using ERB tags to generate a custom data attribute in the <div> tag of the column I was appending the D3 map too. The value of this attribute corresponded to the ID of the carrier in question. The code for that looked like this:

1
<div class="col-lg-12 text-center" id="metro-map" <%= "data-params-id=#{params[:id]}" if params.has_key?(:id) %>>

The AJAX request uses the .data() function to pass the ID to the controller, which is able to query the database for the carrier in question, and then serve back the needed topoJSON. Upon a successful AJAX request, a second AJAX request is fired to the Carrier controller, this time serving back a static JSON with the information needed to render the map of the USA. The rest of the JavaScript code for rendering the D3 map can be seen here.

Conclusion

As you can see, this is a pretty neat trick for scoping your JavaScript to a particular type of view in your Rails app. Using dynamically generated class attributes in your <body> tag, you can have as many specific JavaScript .ready() functions as you have controller actions and views. And don’t forget - with these dynamic class attributes, you can also create corresponding custom CSS for particular views too.