<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Frontend development
Loading

Hashchange Routing with can.route - Part 2 - Advanced

Hashchange Routing with can.route - Part 2 - Advanced

Justin Meyer

Justin Meyer

Twitter Reddit

This article is the second of a two part series (part1) on CanJS's hashchange routing system - can.route. This part walks through through can.route's advanced functionality. Specifically, it covers:

  • The observeable nature of can.route
  • Defining routes
  • Making controls operate independently
  • Using the delegate plugin for setup and teardown

But first a little motivation!

Motivation - got to keep em seperated

If you're building big apps, the ability to break up an application into discrete parts that know little about each other as possible should be a big concern to you. For small apps, you can probably stick with techniques in part 1.

In big apps, you want to keep routing separate from your controls. For example, in Ruby on Rails, you have a list of defined routes like:

match '/group/:name', :controller => "group", :action => "name"
match ':controller/:action/:id/:user_id'

Rails matches incoming requests against these routes and calls a particular controller and method. This is great because routes can change independently of controller code.

In most JavaScript libraries (likely all others but CanJS), this kind of thing is not possible or difficult. There is often a strong association between the route path and your code. You can see this in the following examples:

// SammyJS
this.get('#/search/:query', function(context) {
   ...
});

// Backbone
var Workspace = Backbone.Router.extend({
  routes: {   
    "search/:query": "search",   
  },
  search: function(query, page) { ... }
});

// CanJS
var Workspace = can.Control({
  "search/:query route" : function(){}
})

You'll notice that it would be difficult to maintain routes independent of the code that runs them. To do this, you'd have to do something like:

window.ROUTES = {
  query: '#/search/:query'
}

// SammyJS
this.get(ROUTES.query, function(context) {
  ...
});

// Backbone - I'm not sure?

// CanJS
var Workspace = can.Control({
  "help route" : function(){},
  "{ROUTES.query} route" : function(){}
})

You might be asking yourself:

Why don't JS libraries do it more like Ruby on Rails?

Answer: The lifecycle of a page request on the server is completely different than a JS app's lifecycle. On the server, you have one input, an http request, and one output, the html (typically) response.

On the client, one event can trigger many different changes. Going from #!customer/4 to #!tasks/7 might mean:

  • Updating the navigation
  • Replacing the list of recipes with a list of tasks
  • Changing a "details" panel to show task 7

Ideally all of these items should know as little as possible about each other. It's even better if you don't have to create a managing controller that knows about all of them.

With CanJS, you can distribute routing functionality.

Observable routes

To help make breaking up routing easy, can.route is a special can.Observe, one that is cross-bound to hash. When the hash changes, the route changes. When the route changes, the hash changes. This lets you:

Listen to changes in a specific property like:

can.route.bind("type", function(ev, newVal, oldVal){

})

Or all properties at once like:

can.route.bind("change", function(ev, attr, how, newVal, oldVal){

})

Change a single property like:

can.route.attr("type","todo")

Change multiple properties like:

can.route.attr({
  type : "task",
  id: 5
})

The observable nature of can.route is particularly useful for allowing widgets to work independently of each other. This, like most things, is easier understood with an example.

Consider a history-enabled tabs widget where multiple tabs can be present on the page at once like the following:

Each HistoryTab is configured with the route attribute it's listening to like:

new HistoryTabs( '#components',{attr: 'component'});
new HistoryTabs( '#people',{attr: 'person'});

HistoryTab uses this to listen to changes in that attribute, activate and show the new tab with:

"{can.route} {attr}" : function( route, ev, newVal, oldVal ) {
  this.activate(newVal, oldVal)
}

The routes are defined with:

can.route(":component",{
  component: "model",
  person: "mihael"
});

can.route(":component/:person",{
  component: "model",
  person: "mihael"
});

Learn more about the example on the CanJS Recipes page.

The important thing to understand from this example is that:

  • Many HistoryTabs can be created and work independently
  • HistoryTabs only cares about the data the hash represents, it is not aware of the defined routes.

This last point is especially important for the next section.

Defining routes

can.route( route, defaults ) is used to create routes that update can.route's attributes. For example:

can.route(":type",{ type: "recipes" })

route is a parameterized url hash to match against. Specify parameterized url parts with :PARAM_NAME like "recipes/:recipeId". If the hash matches the route, it sets the route's attributes values to the parameterized part. For example:

can.route("recipes/:recipeId");
window.location.hash = "!recipes/5";
can.route.attr('recipeId') //-> "5"

defaults is an object of attribute-value pairs that specify default values if the route is matched but a parameterized value is missing or not provided. The following shows a default value filling in a missing parameterized value:

can.route(":type",{ type: "recipes" })
window.location.hash = ""
can.route.attr("type") //-> "recipes"

The following shows defaults being used as extra values when a route is matched:

can.route("tasks/:id",{type: "tasks"})
window.location.hash = "!tasks/5"
can.route.attr("type") //-> "tasks"
can.route.attr("id")   //-> "5"

Using can.route on HistoryTabs, we can specify a pretty url and default tabs to select with the following routes:

can.route(":component",{
  component: "model",
  person: "mihael"
});

can.route(":component/:person",{
  component: "model",
  person: "mihael"
});

This sets up the following behavior:

selected component value selected person value example hash
model mihael "#!" (empty hash)
model or view mihael "#!model"
model brian or justin "#!/brian"
model or view brian or justin "#!model/brian"

Routing Independence

The next example shows a history-enabled issues page. The user can filter issues and select an individual issue to get more information from.

The example shows how separate widgets can respond to overlapping route properties. It creates separate Nav, Issues, and Details controls that respond to filter and id attributes.

Nav responds to filter. Issues responds to filter and id. Details responds to id.

Read more about this example on CanJS's recipes page.

Setup and Teardown

The Observe delegate plugin can add even more power to routes. With it, you can listen to more specific observe changes.

For example, you can listen to when the type property is "issues" like:

can.route.delegate("type=issues","set",function(){
  // CODE
})

Within a can.Control, this looks like

"{can.route} type=issues set" : function(){
  // code
}

You can also listen to when a value is added, set, or removed from can.route like:

"{can.route} module add" : function(){
   // show modules
},
"{can.route} module set" : function(){
  // highlight selected module
},
"{can.route} module remove" : function(){
   // remove modules
}

This is used in the following example to show modules when a module is selected (by clicking "Login") and remove them when module is unset (by clicking "Logout").

Conclusion

can.route is not an ordinary url-matching routing helper. It understand that routes reflect the state of an application and allows that state to be represented by an object, letting the developer listen to specific property changes on that object. It does all this, while still providing the basic syntax covered in part1.

can.route reflects the duel nature of CanJS - start easy then scale.