This article is the first of a two part series on CanJS's hashchange routing system - can.route. This part walks through the basics of hashchange routing and how can.route
can be used for basic patterns. Specifically, it covers:
- The basics of history in the browser
- Basic route matching with can.Control
- Parameterized routes
- Creating urls and link with routes
Part 2 covers using can.route
for advanced behavior. Together, these articles should demonstrate that can.route is easy to start with, but scales to much more complex situations.
Hashchange routing and the browser
Hashchange routing in the browser works by listening to changes in the window's hash and updating accordingly. The hash is everything in the url after the first #
. For example, the hash is "#recipes"
in http://site.com/#recipes
. The hash can be read in the browser with window.location.hash
. It can be set like:
window.location.hash = "foo/bar"
It is changed when you click on a link like:
<a href="#recipe/5">Show</a>
You can listen to changes in the hash with the hashchange
event like:
window.addEventListener('hashchange', function(){
console.log('the hash has changed')
})
can.route allows you to:
- Listen to changes in the hash that match a particular pattern (ex:
:type/:id
) and extract useful data from that pattern (ex:{type: "recipe", id: "5"}
). - Create pretty urls.
- Update the route independently of knowing what the route looks like.
- Listen to particular parts of the hash data changing.
Basic routing with can.Control
In a basic application, routing can be done using can.Control's route event. Simply specify the url that you want to match:
Router = can.Control({
"completed route" : function(){
console.log("the hash is #!completed")
},
"active route" : function(){
console.log("the hash is #!active")
},
"project/create" : function(){
console.log("the hash is #!project/create")
}
});
// make sure to initialize the Control
new Router(document);
You can trigger those methods by setting the hash like:
window.location.hash = "!#completed"
Or when you click on a link like:
<a href="#!active">Show Active</a>
Note: can.route
matches hashes starting with #!
to work with Google's Ajax crawling API. This can be used with steal's crawl to produce searchable content for your Ajax apps.
To listen to an empty hash (""
), "#"
, or "#!"
, you can simply write "route"
like:
Router = can.Control({
"route" : function(){
console.log("empty hash")
}
})
Parameterized routes
It's common to run some code every time the url matches a particular pattern. And, you often want the value of the parameterized part(s) of the url. For example, you want the id value every time the hash looks like #!recipe/_ID_
to load the recipe with the corresponding id.
can.route matches parameterized urls by putting a :PARAM_NAME
in the route. It calls the callback function with the parameterized data. The following example loads a recipe by id
, renders it with /recipe.ejs
and inserts it into #recipe
.
Router = can.Control({
"recipe/:id route" : function(data){
console.log( "showing recipe", data.id );
can.view( "/recipe.ejs", Recipe.findOne(data) )
.then( function( frag ) {
$("#recipe").html( frag );
});
}
});
can.route can match multiple parts of the hash. The following matches the type
and id
of the object to be shown and uses the type
to pick a can.Model in Models
.
Router = can.Control({
":type/:id route" : function(data){
console.log( "showing ", data.type," ", data.id );
can.view( "/"+data.type+".ejs",
Models[can.capitalize(data.type)].findOne(data) )
.then( function( frag ) {
$("#model").html(frag)
});
}
});
The order in which routes are setup determines their matching precedence. Thus, that it's possible for one route to prevent others from being matched. Consider:
Router = can.Control({
":type/:id route" : function(data){
console.log(":type/:id",data.type,data.id)
},
":lecture/:pupil route" : function(){
console.log(":lecture/:pupil",data.lecture,data.pupil)
}
});
If the hash is changed to "car/mechanic"
can.route cannot tell which route you are trying to match. In this case, can.route matches the first route - ":type/:id"
. If you encounter this problem, make sure to prefix your route with some unique identifier, for example: "features/:type/:id"
and "classrooms/:lecture/:pupil"
.
Creating urls and links
Within any route enabled app, you need to create links and urls for the user to click on. Use can.route.url(data, [merge])
to create a url for a given route.
can.route.url({ type: "recipe",
id: 6,
route: ":type/:id"})
//-> "#!recipe/6"
If the data contains all the properties in route, you don't have to provide the route name. Example:
can.route.url({ type: "recipe",
id: 6 })
//-> "#!recipe/6"
Additional data properties are added like &foo=bar
:
can.route.url({ type: "recipe",
id: 6,
view: "edit" })
//-> "#!recipe/6&view=edit"
Sometimes, you want to merge additional properties with the current hash instead of changing it entirely. An example might be a history enabled tabs widget. Pass true as the merge option to merge with the current route data:
can.route.url({ tab: "instructions" }, true )
//-> "#!recipe/6&tab=instructions"
Finally, can.route.link(text, data, [props], [merge] )
creates an anchor link with text
:
can.route.link("Edit",
{ type: "recipe",
id: 6,
view: "edit" },
{className : "edit"})
//-> "<a href='#!recipe/6&veiw=edit' class='edit'>Edit</a>"
Conclusion
Using can.route
and can.Control
with the "route" event makes it easy to perform routing basics - pattern matching and creating links and urls. But can.route
scales to handle more difficult and complex situations, especially keeping routing information independent of widgets and controls. See how in Part 2.
Previous Post