Where developers come to connect, share, build and be inspired.

19

Extended Paul Irish's DOM-based Routing for CoffeeScript

3600 views

Have you heard about Paul Irish's "Markup-based unobtrusive comprehensive DOM-ready execution"?. It was written back in the day when client-side MVC was not even a thing - more than a year before Jeremy Ashkenas pushed the first commit to Backbone.js, and no one knew what Ember.js is. However, everyone was already complaining about JavaScript being a language which encourages to write loosely-structured spaghetti code. For those of you who think one-page client-side MVC applications is not panacea, there is something in here to make your life easier.

Does something like this look familliar:

$(document).ready(function(){
  var processMagic = function(){
    // do stuff
  }

  if($("#specific-page-container").length > 0){
    $('body').on("click", ".secreet-link", processMagic)
  } else if($(".another-page-container").length > 0){
    // doo stuff
  }
})

Yuck. It doesn't feel right, it is pain to maintain, and it needs to be addressed. Paul's "DOM-based Routing" and an enhanced "Jason Graber's take on it" offers an elegant solution for multi-page applications that need no fancy client-side MVC framework. Jason Graber puts it straight:

The method is noteworthy in that it enforces a sense of organization in both your JavaScript and your HTML. For sufficiently large web applications, using a system like Paul's can make JavaScript integration a snap. The added benefit of having a structured object containing all your application's functionality is icing on the cake.

But there is something else that enforces a sense of organisation, Object Oriented JavaScript and prevents you from writing bad JavaScript: CoffeeScript! Here at AlphaSights we've put the second layer of Coffee-taste icing on the Graber's cake, and we unanimously agree it is a big win in comparison with the other approaches we had tried before.

This is how we did it in one of our Rails3 applications.

Firstly, let's take a look at the file structure of js files in the "Public" application module.

app/assets/javascripts/public
├── public
│   ├── controllers
│   │   ├── common_controller.js.coffee
│   │   ├── users_controller.js.coffee
│   │   └── events_controller.js.coffee
│   └── models
│       ├── auto_toggle.js.coffee
│       ├── user.js.coffee
│       ├── data_store.js.coffee
│       ├── hider.js.coffee
│       ├── logger.js.coffee
│       ├── notification.js.coffee
│       ├── event_modal.js.coffee
│       └── scroller.js.coffee
└── public.js.coffee

The core routing class for this module is public.js.coffee. Make sure it's loaded by the asset pipeline (or any other technique you use for your .coffee requires)

#  public.js.coffee file
#= require jquery
#= require_self
#= require_tree ./public

window.Public ||= {}; # defining a namespace

Public.initiatedClasses = {} # a variable for caching loaded controller objects

Public.UTIL =
  exec: (controller, action = 'all_actions') ->
    if Public[controller] # try to find a controller
      # create a controller object or re-use it if already present
      klass = Public.initiatedClasses[controller] ||= new Public[controller]

      # check if both a controller and an action are usable
      if typeof klass is "object" and typeof klass[action] is "function"
        klass[action]() # call the function

  init: ->
    body = $("body") # this is where your data-router-class and data-router-action attributes live
    controller = body.data("router-class")
    action = body.data("router-action")

    # CommonController#all_actions is executed on every page, useful for generic stuff
    this.exec "CommonController"
    this.exec controller
    this.exec controller, action


# this is the only place in the whole application module where we bind $(document).ready
# may as well be substituted with pjax or any other events you want
$(document).ready -> External.UTIL.init()

All you have to do to invoke your CoffeeScript classes is to add the following attributes to your body element:

<body data-router-class='User' data-router-action='index'>

# Public.UTIL will call the following functions if they are defined:
#   Public.CommonController#all_actions
#   Public.UsersController#all_actions
#   Public.UsersController#index

This is how we do it in Rails and HAML.

Let's look at the User.js.coffee now:

class Public.UsersController
  constructor: () ->
    @base = $("#main_container")
    @data_sore = new Public.DataStore("users")

  index: ()->
    @hide_stuff()
    @base.on "click", ".show_more", (e) => @show_more_info(e)

  show_more_info: (e) ->
    // process event
    @data_sore.remember(user_id)
    // show stuff

  hide_stuff: ->
    for user in @data_sore.hidden_users()
      $(user).hide()
      // cuddle rainbows

Notice that we don't have to deal with $(document).ready events nor manually check if the code is executed on the page we want it to run on. Neat.

The referenced Public.DataStore class lives in public/modules. It has nothing to do with DOM, it is reusable and easy to test. Of course, you could write the same code in JavaScript as well, but it just feels so natural and easy with CoffeeScript.

All in all, the benefits of using this approach are

  • High level code organisation improvements.
  • In-file code structure improvements.
  • CoffeeScript encourages OO coding style, hence a smaller mental shift required when switching between back and front ends.
  • Rails-like conventions for invoking controller action metods.
  • No need to repeatedly deal with $(document).ready and $(selector).length
  • It's framework independent (apart from the single jQuery-specific $(document).ready call)

Discussion on Hacker News

My twitter @tadas_t

Comments

Add a comment