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]

Monday, 5 March 2012

Versions

It's time I've decided the draft of major and minor release versions for doomhub.com. It will also tell you what kind of functionality you can expect in the future.


[PLANNED] v1.0 first release version
  • Registered users can create projects.
  • The project privacy level (public / restricted / closed) can be decided during creation and edition.
    • Open projects are completely public, all their entries are readable by everyone, all their resources are downloadable by registered users, any registered user can create a map for those.
    • Restricted projects can be read by anyone, but a user has to be invited to the project by a project-admin or project-mod to download a file or to create a map.
    • Closed projects only show on the list, but are not readable at all by uninvited users.
    • Creators of the projects are automatically made admins (project-admins) of them.
    • If a project is closed or exclusive, a project-admin can invite other users to collaborate on respective projects. If a project is open anybody can add stuff for it.
    • Project-admins can make other users project-admins or project-mods.
    • Authorised users (map-authors) can create maps in respective projects.
    • Map-authors can upload files (zip archives) for map, meant to include actual wad files of the maps.

    [PLANNED] v1.1 resources, images and comments
    • Project-admins and project-mods can add resources (like texture packs, text documents, images and stuff) to projects in general, rather than to maps only. They inherit privacy after the respective project.
    • Map-authors can add images (screenshots) to their maps.
    • Users can comment on maps (as long as they have access to see them).
    [PLANNED] v1.2 per item permissions
    • Map-author can specify privacy level per screenshot.
    • Map-author can specify privacy level per wadfile attachment.
    • Project-admin and project-mod can specify privacy level per resource file.

      A more graphic presentation

      Diagrams are always good.
      So, at first I was like:

      But then I was like:
      You can clearly see the WADFILE, wich is a Paperclip attachment has been removed as an attribute from Map class and moved to a separate MapWadfile class.

      Sunday, 4 March 2012

      Slow weekend

      So I created a blog. Setting it up a took like 3 minutes total (including searching for a recommended one)... For the rest of the weekend I was fighting with permissions and attachments in the application. A few problems came out, which was unsurprising really.

      What was surprising, is the fact these were not technical difficulties. I set CanCan (and learnt a lot about it... again) up, then I got Paperclip working with an AWS-S3 account. It was a first time I did something like this and it worked like a charm. Thank god for smart people writing smart software. Even made it possible to limit the uploads and downloads via the usage of HTTPS, rather than standard HTTP. It was all so deliciously simple:

      A User with a proper UserRole can create a Project. A Project is defined by a Game it's designed for (Doom, Doom2, Heretic...) and an Engine (source port) it uses. The User can then proceed to create Maps for it. Each map has a Name (a "display" name), a Lump (the name of the lump it's supposed to be located with in the WAD) and a Wadfile, which actually is a zip archive, oh well. Of course the file name and location has to be automatically determined upon upload, so that it can be both downloaded or removed later with ease. This means the file name has to be static...

      Then it struck me - what if the map author wanted to rename it?
      It's not unheard of - people had done it a few times during the course of DTWiD development. The fact, that I wanted the filenames to be pretty and actually include a slug based on the Map's "display" Name didn't really help. Paperclip can't do this for me. Not on S3.

      I started deliberating what would be better - to store the files with an id-only based names (which would  mean that when downloading people would get files named like so: 2-5-12.zip), or to include additional functionality to change file's name directly in the S3 repository. The first solution was ugly, the second one was resource heavy. Things like that would probably require dedicated worker processes to pull it off and I can't really have those on a shared web hosting.

      Then it struck me again - leaving me barely alive.
      I got it all wrong. Not to mention neither solution would really be reliable enough.
      I just have to separate the Map entity from a Wadfile entity. A single Map will be able to have a couple of related Wadfile class objects, each one of them named automatically, or manually (User's choice) and each of them managed independently, with only create and delete actions to chose from. This means changing the name of the Map will not bother the file at all. The author would then have to decide if he wants to reupload the file with a new name or leave it as is.

      The beauty of it is that it's so much easier to do and it allows a Map to have a couple of files, like different versions of work in progress.
      ...but I should probably limit it to like 5 or 10 files per Map.