Page specific Javascript in Rails 3

Premise

One of the neat features from Rails 3.1 and up is the asset pipeline:

The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages such as CoffeeScript, Sass and ERB.

This means that in production, you will have one big Javascript file and also one big CSS file. This reduces the number of request the browser has to make and generally loads the page faster.

In the case of Javascript concatenation however, it does bring about a problem. Executing code when the DOM has loaded is commonplace in most web applications today, but if everything is included in one big file, and more importantly the same file, for all actions on all controllers, how do you run code that is specific to just a single view?

Solution(s)

Obviously there is more than one way of solving this problem, and rather unlike Rails, there doesn’t seem to be any “best practice” dictated. The closest I found is this excerpt from section 2 of the Rails Guide about the Asset Pipeline:

You should put any JavaScript or CSS unique to a controller inside their respective asset files, as these files can then be loaded just for these controllers with lines such as <%= javascript_include_tag params[:controller] %> or <%= stylesheet_link_tag params[:controller] %>.

And it isn’t even followed by an example, which seems more of an indication, that this isn’t something you should do at all.

Let’s start by this example nonetheless.

Per controller inclusion

By default Rails has only one top level Javascript manifest file, namely app/assets/javascripts/application.js which has the following content:

// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
//= require jquery
//= require jquery_ujs
//= require_tree .

And this is included in the default layout with:

<%= javascript_include_tag "application" %>

N.B. When testing production on localhost, with e.g. rails s -e production, rails by default wont serve static assets, which application.js becomes after pre-compilation, so to avoid any problems when locally testing production, the following setting needs to be changed from false to true in config/environments/production.rb:

# Disable Rails's static asset server (Apache or nginx will already do this)
config.serve_static_assets = true

Now let’s say we have a controller, let’s call it ApplesController, and its corresponding Coffescript file, apples.js.coffee. We might try to include it as per the Rails Guide suggestion like so:

<%= javascript_include_tag params[:controller] %>

And this will work just fine in development mode, but in production produce the following error:

ActionView::Template::Error (apples.js isn't precompiled):

To remedy this, we need to do a couple of things. First off we should remove the require_tree . part from application.js, so we don’t wind up including the same script twice. Just removing the equal sign is enough:

//  require_tree .

To avoid a name clash rename apples.js.coffee to something else, e.g. apples.controller.js.coffee. Then create a new manifest file named apples.js, which includes your coffeescript file:

//= require apples.controller

Lastly, the default configuration of Rails only includes and pre-compiles application.js, so we need to tell the pre-compiler to now also include apples.js. This is also in config/environments/production.rb. Uncomment the following setting, and change search.js to apples.js:

# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
config.assets.precompile += %w( apples.js )

Note that this is a match, so it could also be something like '*.js' in case you have more manifests, which would be the case for per controller inclusion.

Views

The same concept as above could be extended to target individual actions/views of each controller, by having the actions be part of the manifest name. Individual javascript files could then be included like so:

<%= javascript_include_tag "#{params[:controller]}.#{params[:action}" %>

This makes an assumption that all actions on all controllers have a dedicated Javascript file. An assumption which most likely won’t be true in most cases. Another option could be an conditional include like so:

<%= yield :action_specific_js if content_for?(:action_specific_js %>

And then move the include tag to the specific views that need it.

Testing for existence of a page element or class

The DOM loaded event handler could look something like this:

jQuery ->
  if $('#some_element').length > 0
    // Do some stuff here

This could also be a class on body eg.:

jQuery ->
  if $('body.controller_name_action_name').length > 0
    // Do some stuff here

And then then the erb would be like this:

<!DOCTYPE html>
<html>
<head>
  <title>AppName</title>
  <%= stylesheet_link_tag    "application" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body class="<%= "#{params[:controller]}_#{params[:action]}" %>">
 
<%= yield %>
 
</body>
</html>

Function encapsulation and on-page triggering

Instead of registering the handlers for DOM loaded, wrap the necessary code in a function that can be called later and then trigger that function directly in the respective view.

There is one thing we need to consider though. All Coffeescript sources for each controller get wrapped in it’s own closed scope, i.e this Coffeescript in apples.js.coffee:

apples_index = ->
  console.log("Hello! Yes, this is Apples.")

Becomes:

((
  function(){
    var a;
    a=function(){
        return console.log("Hello! Yes, this is Apples.")
    }
  }
)).call(this);

So in order for us to have a globally callable function, we must first expose it somehow. We can do this by attaching the function to the window object. Changing the above code like so:

window.exports ||= {}
window.exports.apples_index = ->
  console.log("Hello! Yes, this is Apples.")

If we insert this line in application.html.erb layout just before the closing body tag:

<!DOCTYPE html>                                                                           
<html>                                                                                    
<head>                                                                                    
  <title>AppName</title>                                                                   
  <%= stylesheet_link_tag    "application" %>                                             
  <%= javascript_include_tag "application" %>                                             
  <%= csrf_meta_tags %>                                                                   
</head>                                                                                   
<body>                                                                                    
 
<%= yield %>                                                                              
 
<%= yield :action_specific_js if content_for?(:action_specific_js) %>                     
</body>                                                                                   
</html>

We can now call the exposed function directly from our view like so:

<% content_for :action_specific_js do %>
<script type="text/javascript" language="javascript">
  $(function() { window.exports.apples_index(); });
</script>
<% end %>

Wrap up

Neither of these three examples is a “one-fit-all” solution I would say. Dividing up the Javascript source will start to make sense as soon as the Javascript codebase grows past a certain size. It might be interesting to test out, just how big that size is on a certain bandwidth, but I think that’s out of the scope for this post.

Given the fact that there isn’t really a defined best practice yet, perhaps the ruby community will come up with something better than the examples I presented here. In my opinion I think this is definitely something that could be better thought out.

11 thoughts on “Page specific Javascript in Rails 3”

  1. I like the approach of exporting per-controller or per-action javascript to a module on window. This eliminates the overhead of executing the dom-ready handler for code that isn’t going to be used in a given context.

    It does seem like extra maintenance overhead, but I imagine just a little tooling could improve that situation.

    Reply
  2. Thanks for this write up. I have been noodling on how to really intermix CoffeeScript into my apps in a kind of railsy way, and feel like the pattern hasn’t quite been identified yet. I like your solutions as they seek a generic, low-code, DRY, convention-driven approach … which is to say Railsy 🙂

    What about this approach: in the layout, replace the tag with — similar to adding a class, but gets you a data-view attribute.

    So now, you can have a generic coffeescript method like

    view_method = (name, params = ”) ->
    method = $(“body”).data(‘view’) + ‘_’ + name + ‘(‘ + params + ‘)’
    eval(method) if method?

    And then, in the controller-level file (e.g. posts.js.coffee) create a method following a naming convention

    posts_show_doSomethingSpecial = (param) ->
    alert “Well howdy, pardner!”

    which is now callable from the document.ready context, i.e. also in posts.js.coffee, call

    $ ->
    view_method(“doSomethingSpecial”)

    True, if will be called for all of the views (posts#index, posts#new, posts#create, etc.) but will only apply to the specific view it’s intended for.

    Dunno. I like the idea of passing useful information accessible via coffeescript/JS in the data-view attribute. Not sure if the rest is right. But it works 🙂

    Reply
    • I think you are on to something with regards to using the data-attribute. Regarding naming conventions you could maybe even take it a step further and then *always* execute a method named after controller_action on dom loaded:

      application.js.coffee:

       
      jQuery ->
        view_method = $(“body”).data(‘view’) + ‘_’ + name +(+ params +)'
        eval(method) if method?

      And then still define your various

      posts_show_doSomethingSpecial = (param) ->
        alert “Well howdy, pardner!

      In your controller js files. That way you only have the on dom loaded trigger in one place.

      Reply
      • Right — this way there’s never any ambiguity or extra work it’s there if you need it. I have been trying to grok backbone.js (or specifically the rails-backbone gem, which provides a nice file layout and EJS templates), and then in comparison to knockout.js which uses the data- attributes to bind values to elements. I think I am looking for something in between — not everything has to be done in JS all the time.

        The other thing that crossed my mind was instead to pass the whole @post object, and using rails naming conventions, populate the fields from JS, rather than ERB. This might be overkill.

        Anyway, thanks for your great post. I was feeling like I was missing something, but it seems like we’re all kind of getting how Rails and JS and CoffeeScript can all play together nicely and effectively.

        Reply
  3. The Rails core team has never really understood how to deal properly with JavaScript (remember the horrible inline helpers in Rails 2?), and that seems still to be the case — the idea that a whole site’s JS should get munged together into one file is IMHO laughable. Fortunately, the asset pipeline lets us use controller- or page-specific JS includes if we want to, and still get the benefits of the asset pipeline (like easy CoffeeScript compilation), so my plan is simply to ignore the recommendation to include all the JS everywhere, and use the *good* parts of the pipeline.

    Reply
  4. Many thanks for a very useful post!

    When using the controller_name_action_name class attribute for the body element, I was wondering why you were testing for the length of $(‘body.controller_name_action_name’) rather than using jQuery’s hasClass:

    if $(‘body’).hasClass(‘controller_name_action_name’)
    //do some stuff

    Cheers,
    Henry

    Reply
  5. You’re welcome.

    I don’t know really, I guess I just continued down the road from the previous example. But you are right, using hasClass, might be a bit more expressive.

    Reply
  6. Hi, just want to share our solution for this problem.

    We have created this gem, Paloma (https://github.com/kbparagua/paloma), to solve this specific issue.

    It gives you the ability to execute page-specific javascript without the need to load different javascript files. So basically you still get the performance boost provided by the asset pipeline.

    It also follow the view structure of Rails. Like this:

    assets/
    javascripts/
    paloma/
    controller_name/
    action_1_name.js
    action_2_name.js

    So you can easily find what javascript code that will execute depending on the page you are dealing with.

    Anyway, here’s a quick example on how to use it:

    javascript file:

    Paloma.callbacks[‘users/new’] = function(params){
    // This will only run after executing users/new action
    alert(‘Hello New Sexy User’);
    };

    Rails controller file:

    def UsersController < ApplicationController
    def new
    @user = User.new
    # No special function to call, the javascript
    # callback will be executed automatically
    end
    end

    That's just it. Simple and Logical organization of your Javascript files.

    Reply
  7. Thanks for the post!

    Typo found: “// require apples.controller” you surely mean “//= require apples.controller”

    😉

    Reply

Leave a Comment