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.
- Sharing data between your Rails application and your CoffeeScript code.
- Arranging your code in a structured way.
- Executing the correct code on the correct page, without needing to check the DOM in every CoffeeScript file.
- Reusing elements on multiple pages.
- 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.