Thursday 5 April 2012

Adding backbone to Rails 3.1 part two

[Read part one here: Adding backbone to Rails 3.1 part one]

Picking up from previous part

Last time we learnt how to add Backbone.js support to a clean Rails 3.1 application. Even though the application I was basing on already has some content, everything concerning Backbone can be done on a freshly created app.

We ended up having an Initializer class written in Coffeescript that runs Backbone on every Rails view. Now we'll modify add router-based functionality. Simply put: routers are to Backbone what controllers are to Rails. What I'm going to show is how to link controllers with appropriate Routers, so that every time you call a view or action from a Rails controller, Backbone will try to locate corresponding views through an adequate router.

e.g. when I request maps/index from Rails application, upon rendering in my browser, the said view will also call JavaScripts that are located in Doomhub.Backbone.Views.Index view, served through Doomhub.Routers.Map router. To make things even prettier (author`s opinion) there should also be a Root ...router, The-One-To-Rule-Them-All router, a Main one.

The Main Router

I think that for readability and convention's sake a main router is a great thing, it keeps information about all the other routers in one place. Thanks to this, the Initializer can always just blindly call the Main router and pass it some parameters we dug up from Rails server (@controller and @action we defined in the first part), so that the router can work its Backbone magic.

Earlier I said routers in Backbone are like controllers in Rails. Actually they also assume the role of Rails router mechanism (hence Backbone prefers to use this name). In Rails the server routes traffic to specific controllers, basing on the information specified in config/routes.rb file. Since JavaScript is obviously client-based, Backbone distributes its own routing mechanism among the router classes. By having a Main router that redirects to other "named" routers in our Backbone we retain some of the Rails' characteristics we might be so used to, while not going completely out of Backbone's way.


app/assets/javascripts/backbone/routers/main_router.coffee:

class @Doomhub.Routers.Main extends Backbone.Router

  routes:
    'maps/:action': 'maps'

  maps: (action) ->
    @maps_router ?= new Doomhub.Routers.Map()
    @maps_router[action]() if @maps_router[action]?



As one can see, the Main router defines a routes hash, with :router/:action (routing string) as a key, and a method (function) name as its value. This hash actually serves no value for our mechanism, other than information, but it's very Backbone'ish. A pure Backbone application would rely on this hash to conduct routing. You can actually drop it completely. The approach I will use is more... primitive.


Writing a new Helper class ...helps:


app/assets/javascripts/backbone/helper.coffee:



class @Doomhub.Helper
  constructor: ->
    @controller = $('body').data('controller')
    @action = $('body').data('action')



It also requires modifications done to the Initializer class:


app/assets/javascripts/backbone/initializer.coffee:



class @Doomhub.Initializer
  constructor: ->
    window.H = new Doomhub.Helper
    H.initializer = @
    @router = new Doomhub.Routers.Main()
    @run_backbone H.controller, H.action

  run_backbone: (router, action) ->
    console.log "router: #{router} / action: #{action}"
    if @router[router]
        @router[router](action)


Once an instance of Initializer class is created (and because of new Doomhub.Initializer() in the app/assets/javascripts/backbone/doomhub.coffee file it always is, when a proper Rails view is called) it creates a new Main router and calls run_backbone method, which - in turn - will call for a proper action in a related router if it exists.

The last links of the chain

Now we have means to direct the JavaScript flow to a specific point in the Backbone structure, it's high time we build this point.


There needs to be a router we delegate to from the Main router.


app/assets/javascripts/backbone/routers/map_router.coffee:


class @Doomhub.Routers.Map extends Backbone.Router
  
  index_action: ()->
    @view ?= new Doomhub.Views.Maps.Index({ el: $(body) })


Map router creates a new instance of Index View. The constructor requires an HTML element to cling to, as all the events and functions within this view will be limited to this element. You can pass the entire body as the base element, but you really should have a unique container with an id.

app/assets/javascripts/backbone/views/maps/index.coffee:

class @Doomhub.Views.Maps.Index extends Backbone.View

  events:
    'click #my_button': 'do_something'

  do_something: (e) ->
    alert 'ouch'

This is basically the end of the road. The code for the Index view should be self-explanatory, provided you know anything about Coffeescript, and you support yourself with the Backbone.js documentation.

Sunday 1 April 2012

Adding backbone to Rails 3.1 part one

RAILS Y U NO GROW A SPINE

Since the first time I used (okay, was forced to understand and use) Backbone.js I was completely mesmerized by it. MVC architecture driven by one of the most powerful (author's opinion) languages out there? Yes. Please.

However the project I was working on used Backbone as an addition, supplement... a non-stand-alone add-on (if you prefer game terminology) to existing Rails 3-ish architecture. Because Backbone has become very popular - in what is to be perceived as a very short period of time (relative to other technologies) - there are many great tutorials - including official ones - on how to start building a complete Backbone-based MVC application.

...but I could't find anything concrete on how to add full Backbone support to a Rails app, while still keeping it a Rails app. How to replace all the awful unobtrusive JavaScripts with beautiful Coffeescripts governed by a smart framework.

A friend of mine (@michaltaberski) helped me to understand the concepts, but after that it was good-ol' copy-paste from the projects I worked on... and even better-older trial-and-error.



So let's start where all good things start.
No, not in a bar.
At the beginning.


Adding Backbone to an existing Rails 3.1 app

At the beginning there was an ordinary Rails 3.1 application. It had an asset pipeline and all that good jazz you'd expect from 3.1. Among the most important gems, I guess, there would be:

gem 'rails', '3.1.3'
gem 'haml-rails'
gem 'jquery-rails'
gem 'json'
gem 'rails-backbone'
gem 'therubyracer'

Of course I ran all the required rails g foobar:install generators if there were any (and there definitely was one for Backbone). It added STUFF to my app/assets/javascripts/application.js and also created a directory tree under app/assets/javascripts/backbone/. There's also a very important file app/assets/javascripts/backbone/doomhub.js.coffee.
Now I wasn't really sure if I was too happy with it, but I really don't want this blog entry to evolve into "where-your-files-should-go" discussion, so I let them be.

At this point I could run the rails server, view the app and tell right away (by calling Backbone from the JavaScript console in a browser), that the framework is included.
Now I needed a way to make it fire up proper backbone-actions in proper backbone-routers, in response to rails server requesting rails-actions.
How to connect two different MVCs; how to force one of them to - basically - be a slave of the other one? There are probably so many ways to do this, and probably even more really ugly ways to do this. I'm quite happy with the solution @michaltaberski suggested.



Making Backbone run automatically

Have your application.html.haml body tag include some additional data:


%body{:data => { :controller => controller_name, :action => action_name  }}


This will serve as an "anchor" for Backbone runner class (created manually in... just a moment), which will read the controller/action parameters, and push them to a corresponding router/action combo.

Next I created the Initializer (runner) class file in app/assets/javascripts/backbone/initializer.coffee:

class @Doomhub.Initializer
  constructor: ->
    alert 'oh my god yes'


Now I had to find a way to run it on every Rails request. application.js seemed like a good place, but when I added new Doomhub.initializer() or anything along those lines in there, it was ignored, tho' the same thing called in JavaScript console created an Initializer properly.
Then I felt that even if I did manage to find how to run it from application.js, that wasn't really a good place for this code to be. I knew a better location.


I took  app/assets/javascripts/backbone/doomhub.js.coffee and modified it like so:

#= require_self
#= require_tree ./templates
#= require_tree ./models
#= require_tree ./views
#= require_tree ./routers

@Doomhub ?= {}
@Doomhub.Routers ?= {}
@Doomhub.Views ?= {}

$ ->
  init = new Doomhub.Initializer()


This was more like it. The code felt in place and - most importantly - it was working.


...and I also removed the js portion of the file name, because I hate it.
Then I renamed all my files to pure coffee and it felt great.
application.js required translation to Coffeescript format to make it work, so I changed all "//" into "#/"... and that was it.

Earlier in this "chapter" we made controller_name and action_name accessible through data attribute in the application's main layout's body tag. Now why would we do that, if we didn't plan on using it later?
Let's modify the Initializer class:


class @Doomhub.Initializer
  constructor: ->
    @controller = $('body').data('controller')
    @action = $('body').data('action')
    console.log(@controller)
    console.log(@action)


In the next episode we'll modify the Initializer to automatically use a Router-based structure, with one main general Router ...routing requests to other Routers below it, to finally call a desired action.
We will also create a handy Helper class.

Ta-ta.

[Read part two here: Adding backbone to Rails 3.1 part two]