My new blog here at www.mostovenko.com

Tuesday, August 14, 2012

url_for for Spine.js

Recently i discovered a new wonderful language  - Coffeescript. As front page of Coffeescripts website says - "it's a little language that compiles into javascript". Good it's or not, but nowdays javascript and HTML5 are the most convenient tools for implementing rich client web application. And Coffeescript makes our life much easier.

Spine.js it's a great choice if you building your client side on coffeescript. It's written in coffeescript, it's source code is easy to read and understand. And another thing that i like in Spine.js - is that it can be easily extended.

And, after playing some time with it, i found that it misses one useful feature - possibility to generate url links dynamically =(. As in some python web frameworks, like Pylons, Flask or Django, when we know the name of controller and the action - we can generate url for some handlers in templates. I have not found how i can do this in Spine.js - so i decided to implement it by myself.



Spine.js has it's own routing logic. It allows us to bind hash urls to some anonymous functions. But i wanted something different. I wanted to provide the ability to get controllers handlers url by passing it's name and name of the appropriate handler. Something like this :

omg_url = helper.url_for("MyController", "omg_action")

And than i will not need to hardcode  urls in the code. Before i start to explain how i was reinventing the wheel i want to warn you that i am not a javascript guru, and i will be glad to recieve some feedback with argumented critics =)

So, enough of lyrics let's get started:

First of all i thought that we need some kind of global container that will hold our controller instances or links to them. As i came from the .NET world, the first idea was to implement logic that will produce container and bootstrapper as separate classes. Container will hold all controllers instances and bootstraper will load them. I even created them as separate classes, very simple and it worked,  but than i thought a little and decided to use dynamic power of javascript  and implement dynamic bootstrapper that will automatically register only that controllers that have actions. This approach seems to me more flexible and beautiful. Here is what i've got instead of heavy bootstrap class :

 add_actions = (controller, actions) ->
  controller.actions = {} unless controller.actions

  Spine.container or=[]

  Spine.container.push controller unless \
    controller in Spine.container

  exec = (func_name) ->
      ->
          controller[func_name] arguments...
          controller.active()

  for route, func_name of actions
    if typeof controller[func_name] == "function"
      controller.actions[func_name] = route
      controller.route(route, exec func_name)


 Spine.Controller::add_actions = add_actions

I added add_actions function to Spine.Controller prototype. It dynamicaly adds action property if Controller doesn't have it, registers controllers. It accepts the dict  with such structure - ("[url pattern]" : "[method name]" ) iterates through it, creates appropriate executable functions and binds this function with the help of Spine.Route methods. Nothing special but works)

Then url_for func will be look like:

url_for = (controller_name, action, parameters...) ->
  ControllerType = require "controllers/#{controller_name}"
  _controller_name = new ControllerType().constructor.name

  hashStrip    = /^#*/
  namedParam   = /:([\w\d]+)/g
  splatParam   = /\*([\w\d]+)/g
  escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g

  for controller in Spine.container
    if controller.constructor.name == controller_name
      url_path = controller.actions[action]
      url_path = url_path.replace(hashStrip, '')
        .replace(splatParam, '')
        .replace(namedParam, '')
        .replace(escapeRegExp, '')
      url_path += parameters.join('/')

  url_path

Spine.h.url_for = url_for

Url function accepts controller name, name of action, and additional parameters for passing into url. First two lines ensure us that we working with real controller name(in case if source scripts will be minified). Than we are iterating through controllers in global container, looking for action. When we found appropriate action, we need to construct url. As in Spine.js source code we are using several reg. expressions to get url from pattern and than applying parameters to it. And thats all.

To use this we need to include script with this two function somewhere in our spine app and start using actions in our controller.
For instance, let's imagine that we have implemented controller PostsController and it has several actions (view, edit, add)


class PostsController extends Spine.Controller
    constructor: ->
        @actions @,
            'posts/view' : 'view'
            'posts/edit/:id' : 'edit'
            'posts/add' : 'add'

    view: (args...) ->
        # some view logic here.

    edit: (args...) ->
        id = args[0].id
        # some edit logic here.
    
    add: (args...) ->
        # add logic...

As we see everything looks nice.

More detailed about usage and sources are available here github link

Thanks for reading)