Rails: Page Specific JavaScript with Asset Pipeline

After being away from Rails for several years, coming back to Rails 3/4 and its asset pipeline was a challenge at times.  While the performance gains and automation are quite valuable, the asset pipeline isn’t without its occasional challenges.  One of those challenges was the growing amount of JavaScript being executed on every page of our site.  While this mostly amounted to at worst errors in the console, it just felt icky (that’s a technical term).

Upon exploring Stack Overflow and various blog posts, I finally found a solution that felt somewhat better: Page-Specific Javascript using Coffeescript.  The overall solution was fairly clean, didn’t cause an explosion in the number of JS files, and also did not require an update to the config each time a new JS file was added.  For larger, more modular needs, other solutions (e.g. this one) may be more applicable, but this approach works fine for our medium size app.

I will not review the entire approach verbatim here, but I did make several changes to the aforementioned article.  It’s important to note that this solution utilizes the object-oriented nature of CoffeeScript to simplify life.

First, we will create a base class that houses the common code executed on every page.  I named this class base.js.coffee (in app/assets/javascripts).

window.MyApp ||= {}  # namespace for your app, replace as you desire
class MyApp.Base
  constructor:() ->
    # common code we want to run on every page
    console.log('I print on every page!')
    # be sure to return this
    this

Next, we can begin to build controller-specific classes on top of the base class.  Like Rails, we will use the convention of the naming the class the same as the accompanying controller.  For example, our CommentsController will be accompanied by a CoffeeScript class called MyApp.Comments in the file comments.js.coffee.

window.MyApp ||= {}
class MyApp.Comments extends MyApp.Base
  constructor:() ->
    super  # call Base class for core functionality
    this   # and be sure to return this

  # now, we enumerate the actions for which we want page-specific behavior
  index:() ->
    console.log('I only print on the comments#index page')
  show:() ->
    console.log('I only print on the comments#show page')
  edit:() ->
    # something special for the comments#edit page

Continue to build a CoffeeScript class for each of your controllers that require JavaScript beyond whatever your Base class takes responsibility for.  Be sure to include a method for each action/page on which unique functionality is required.

Finally, we need to bring everything together and ensure our code is called wherever necessary.  One approach would be to include the following in your application’s layout view, though it could also be included anywhere sure to be called.

<%= javascript_tag do %>
$(document).ready(function() {
  window.$M = new (MyApp.<%= params[:controller].capitalize %> || MyApp.Base)();
  if (typeof $M.<%= params[:action] %> === 'function') {
    return $M.<%= params[:action] %>.call();
  }
});
<% end %>

The above first instantiates a new instance of our controller’s accompanying class and then attempts to call the action method, if it exists.  The approach used in our app is slightly different, but this works as well.

Note that I did run into an issue using Turbolinks in conjunction with the above approach that we are yet to solve.  In short, despite using jquery.turbolinks and other known approaches, the action methods were being called multiples times per page.  I have seen mention of this behavior when Turbolinks is used in conjunction with anonymous functions (e.g. those created by CoffeeScript), but have not found a fix.  Please comment if you can help.

Advertisements

4 Comments on “Rails: Page Specific JavaScript with Asset Pipeline”

  1. Teddy M. says:

    Thanks for this! Very helpful indeed, especially for filling in some of the gaps in the other article. In particular, using “extends MyApp.Base” instead of just “Base” made all of my calls to super work properly: huzzah!

    Here’s what I did to get this working properly with Turbolinks:

    In the header of my layout, I included the line:
    $(document).on(‘page:fetch’, function(){
    window.$W = “”
    })

    This ensures that every time a page is fetched via Turbolinks, the $W variable is cleared.

    In the body of my layout, I wrapped your code in a function called ready. I then included the following two bindings to the $(document) object:
    $(document).on(‘ready’, function() {
    ready()
    })

    $(document).on(‘page:load’, function() {
    if (window.$W == “”) {
    ready()
    }
    })

    Thus the “ready” action will always call the function, but page:load will only call it if the page:fetch action has been invoked and cleared the $W variable.

    I’ve tested it out on my app and it seems to work exactly as I want it to, only calling the scripts I want on the right pages. Thoughts?

  2. josh says:

    Teddy – thanks for the comment. I didn’t look closely at Turbolinks for this project given some past difficulties, but it didn’t not work functionally from what I saw.

    Did you look at https://github.com/kossnocorp/jquery.turbolinks? It’s supposed to be a drop-in replacement, though again, it didn’t work perfectly for me on a past project.

  3. Teddy M. says:

    I have looked at jquery-turbolinks, but it didn’t work at all with the above solution for page-specific scripts, so I scrapped it in favor of my solution, which seems to be working well so far.

  4. josh says:

    Your solution does look promising, I will give it a go when needed.