CoffeeScript and Rails, Happy Together

Here at aTech Media we believe in progressive enhancement. We like to start with a working application and use client-side code to build additional, helpful functionality. Naturally, since we're big Rails fans, we like to use CoffeeScript to build our Javascript.

There are a few main challenges that I've identified throughout my time writing CoffeeScript for a few applications.

  1. Sharing data between your Rails application and your CoffeeScript code.
  2. Arranging your code in a structured way.
  3. Executing the correct code on the correct page, without needing to check the DOM in every CoffeeScript file.
  4. Reusing elements on multiple pages.
  5. Knowing which application endpoints to hit on an AJAX request.

In this blog post, we'll look at a couple of gems that we've been using in the Deploy 2.0 build to solve these challenges, Rails Script and CoffeeRoutes.

Introduction to Rails Script

Rails Script is a lightweight javascript framework for working with CoffeeScript and Rails. It leverages CoffeeScript classes to give you a Rails-controller like way of dealing with your JS.

Rails Script gives us a number of features:

  • A controller-like structure which maps to your application's underlying controllers, which allows us to specify CoffeeScript to run on a per-application, per-controller or per-action basis.
  • A way to define reusable components for your pages using Utilities and Elements.
  • A structured way to organise your CoffeeScript.
  • Rails generators to quickly build stubs for new components of your application.
  • Helper methods to assist in passing data from your Rails application to your CoffeeScript.
File Structure

Rails Script by default gives you a structure with one file for each of your Rails Controllers and a directory to contain your utility classes and another for your elements. One file per controller might sound like it will get out of hand, especially with pages which use a lot CoffeeScript, but by abstracting out that functionality to individual elements it's not a problem at all.

├── base.js.coffee
├── dashboard.js.coffee
├── deploy.coffee
├── deployments.js.coffee
├── elements
│   ├── feedback_form.js.coffee
│   ├── live_deployment_status.js.coffee
│   ├── server_protocol_form.js.coffee
│   └── sortable_project_list.js.coffee
├── global.js.coffee
├── projects.js.coffee
├── repositories.js.coffee
├── users.js.coffee
└── utilities
    ├── all_checkbox.js.coffee
    └── progress_dialog.js.coffee
Controllers

The controllers in Rails Script behave just like those in your Rails application, they inherit from a base controller and define methods for each action that controller is responsible for. Rails Script will automatically execute the correct action for each page.

Use the handy generator to create you a stub for your controller.

rails g rails_script:controller Projects  

Your newly generated file should look something like this:

window.App ||= {}  
class App.Projects extends App.Base

  constructor: ->
    super
    return this

  beforeAction: (action) =>
    console.log "before #{action} action"

  index: =>
    @projectList = new Element.SortableProjectList()
    return
  ...

You can see in our index action that we use the sortable project list element, we'll come on to this in a moment.

You can also define beforeAction and afterAction methods which will be executed around your action specific code. Structuring your CoffeeScript this way means that you'll always know which file to go to to modify the javascript behaviour of a particular page.

Element Reuse

In our previous example we used an Element to attach a particular set of behaviour to an action. Elements define the functionality for one particular element on a page. In this case, we made a list of projects sortable. Lets take a look at that element.

window.Element ||= {}  
class Element.SortableProjectList 

  constructor: ->
    @projectList = $('.js-project-list')
    @sortForm = $('#user_project_sort')

    @sortForm.find('.js-project-sort').on 'change', 'input[type=radio]', @sortProjectList

    return this

  sortProjectList: (e)=>
    # Sort the elements in @projectList
    ...
    @updateSortPreference()

  updateSortPreference: =>
    # Update the users preference on the server
    ...

In our constructor we identify the elements we're interested in, the list itself and the inputs that define the order the list is sorted in. We also attach an event listener to the inputs which calls the method sortProjectList, which in turn calls another method we've defined in the class.

Breaking our elements out into files like this allows us to keep our controllers looking clean, and keeps all of the behaviour linked to a particular element in one place. If you have multiple elements that need to interact with each other you can just pass element instances between each other.

Passing Data to your CoffeeScript

Passing data to your CoffeeScript is now extremely simple, just call the to_javascript helper in your Rails app and pass it a hash and that object will be available in your CoffeeScript

# projects_controller.rb
to_javascript :project_permalink => @project.permalink, :username => "dan"  
# base.js.coffee
Utility.RailsVars.username  
=> "dan"

There's nothing particularly clever about what rails_script does, but having a working structure formalised, along with all of it's helpful generators can assist in keeping your CoffeeScript sane.

Introduction to CoffeeRoutes

CoffeeRoutes addresses the last problem identified in the introduction. Traditionally, if you wanted to make an AJAX request back to your Rails application from CoffeeScript you either had to hardcode the URL or read it from another DOM element on the page.

CoffeeRoutes attempts to solve this problem by exposing your Rails named routes to your CoffeeScript, and providing your with helper methods to access them.

With CoffeeRoutes installed, you can call your routes exactly like you would from your Rails view or controller.

project_deployments_path({"project_id": "my-project"})  
=> "/projects/my-project/deployments"

Happy Together

If you combine the exposure of named routes to your CoffeeScript with the ability of RailsScript to pass variables then your project_id etc. can be automatically generated with a little help.

In Deploy, we have a controller helper method which locates database objects for use in our controllers, we just extended that to automatically write the the parameters used out to the CoffeeScript.

class ApplicationController < ActionController::Base  
  private

  def locate(resource, scope, options = {})
    options[:param] ||= :id
    options[:field] ||= :identifier

    return unless params[options[:param]]

    result = scope.where(options[:field] => params[options[:param]]).first
    if result
      instance_variable_set "@#{resource.to_s}", result
      to_javascript resource => params[options[:param]]
    else
      raise ActiveRecord::RecordNotFound
    end
  end
  ...
end  
class ProjectsController < ApplicationController

  before_action do
    locate :project, current_account.projects, :id
  end
  ...
end  

Now, in our previous example we can access the project ID automatically.

project_deployments_path({"project_id": Utility.RailsVars.project})  
=> "/projects/my-project/deployments"

Using Rails Script and CoffeeRoutes have allowed us to take a collection of poorly organised javascript files and hacks for passing data to the JS and rewrite them in a coherent, extensible way, inline with current Rails conventions.