Canjs Route2

canjs-route

How can we use can.route?

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"}).
  • Update the route independently of knowing what the route looks like.
  • Listen to particular parts of the hash data changing.

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")
  }
})
var Routing = can.Control({
  ':type/:id route': function( data ) {

  }
})

new Routing( document.body );
can.route.attr( { type : 'todos', id: 5 } )

It allows single page applications to provide pretty urls and easy back button support.

It is a special Observe that updates window.location.hash when its properties change and updates its properties when window.location.hash changes. You can give can.route a template to translate URLs into property values, but if no route is provided, it just serializes the route into standard URL-encoded notation.

How can we use can.route without a template?

// Before we start, empty the hash.
window.location.hash = '';

// This means that can.route is empty.
can.route.attr(); // {}

// Set the hash...
window.location.hash = '#!id=7';

// ...and can.route reflects that.
can.route.attr(); // {id: 7}

// Set the route...
can.route.attr({type: 'todos'}, true);

// ...and the hash reflects that.
window.location.hash; // #!type=todos

// Set a new property on the route...
can.route.attr('id', 6);

// ...and the has changes again to reflect multiple properties.
window.location.hash; // #!type=todos&id=6

How can we use can.route with a template?

If you give can.route a template, you can make pretty URLs:

// Give can.route a template.
can.route(':type/:id');

// If you set a hash that looks like the route...
window.location.hash = '#!todos/5';

// ... the route data changes accordingly.
can.route.attr(); // {type: 'todos', id: 5}

// If the route data is changed...
can.route.attr({type: 'users', id: 29});

// ...the hash is changed using the template.
window.location.hash; // '#!users/7'

// You can also supply defaults for routes.
can.route('', {type: 'recipe'});

// Then if you change the hash...
window.location.hash = '';

// ...the route data reflects the defaults.
can.route.attr(); // {type: 'recipe'}

How can we listen to can.route events?

Because can.route is an Observe, you can bind to it just like normal Observes:

can.route.bind('id', function(ev, newVal, oldVal) {
    console.log('The hash\'s id changed.');
});

You can listen to routing events in Controls with the route event:

var Routing = can.Control({
    'route': function() {
        // Matches every routing change, but gets passed no data.
    },
    'todos/:id route': function(data) {
        // Matches routes like #!todos/5,
        // and will get passed {id: 5} as data.
    },
    ':type/:id route': function(data) {
        // Matches routes like #!recipes/5,
        // and will get passed {id: 5, type: 'recipes'} as data.
    }
})

What is can.route.url?

can.route.url takes a set of properties and makes a URL according to can.route's current route.

can.route(':type/:id', {type: 'todos'});
can.route.url({id: 7}); // #!todos/7

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"

How can we merge additional properties with current hash?

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"

The above code takes an object and adds all of its properties to the URL.

What is can.route.link?

can.route.link does the same thing as can.route.url, but it returns an anchor element (in string form) ready to be inserted into HTML. You can also specify extra properties to be set on the element.

var a = can.route.link(
    'Todo 5',
    {id: 7},
    {className: 'button'}
);
a; // <a href="#!todos/7" class="button">Todo 5</a>
can.route.link(text, data, [props], [merge] )
can.route.link(
    "Edit",
    {
        type: "recipe", 
        id: 6,
        view: "edit" 
    },
    {className : "edit"}
)
//-> "<a href='#!recipe/6&veiw=edit' class='edit'>Edit</a>"

What is parameterized route?

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"

How can we create URLs?

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>"

What is observable route?

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. The observable nature of can.route is particularly useful for allowing widgets to work independently of each other.

can.route("recipes/:recipeId");
window.location.hash = "!recipes/5";
can.route.attr('recipeId') //-> "5"
can.route(":type",{ type: "recipes" })
window.location.hash = ""
can.route.attr("type") //-> "recipes"
<ul id="components" class="tabs ui-helper-clearfix">
  <li><a href="#model">can.Model</a></li>
  <li><a href="#view">can.View</a></li>
  <li><a href="#control">can.Control</a></li>
</ul>
<div id="model" class="tab">
    can.Model is used for getting data and listening to it.
</div>
<div id="view" class="tab">
    can.View is used to present data and update with changes to it.
</div>
<div id="control" class="tab">
    can.Control is used to listen to things.
</div>
<br/><br/>
<ul id="people" class="tabs ui-helper-clearfix">
  <li><a href="#brian">Brian</a></li>
  <li><a href="#justin">Justin</a></li>
  <li><a href="#mihael">Michael</a></li>
</ul>
<div id="brian" class="tab">
    <img src='http://bitovi.com/images/people/brian.jpg'/>
</div>
<div id="justin" class="tab">
    <img src='http://bitovi.com/images/people/justin.jpg'/>
</div>
<div id="mihael" class="tab">
    <img src='http://bitovi.com/images/people/mihael.jpg'/>
</div>

// JavaScript:
var HistoryTabs = can.Control({
  init: function( el ) {

    // hide all tabs
    var tab = this.tab;
    this.element.children( 'li' ).each(function() {
      tab( $( this ) ).hide();
    });

    // activate the first tab
    var active = can.route.attr(this.options.attr);
    this.activate(active);
  },
  "{can.route} {attr}" : function(route, ev, newVal, oldVal){
    this.activate(newVal, oldVal)
  },
  // helper function finds the tab for a given li
  tab: function( li ) {
    return $( li.find( 'a' ).attr( 'href' ) );
  },
  // helper function finds li for a given id
  button : function(id){
    // if nothing is active, activate the first
    return id ? this.element.find("a[href=#"+id+"]").parent()  : 
                this.element.children( 'li:first' );
  },
  // activates 
  activate: function( active, oldActive ){
    // deactivate the old active
    var oldButton = this.button(oldActive).removeClass('active');
    this.tab(oldButton).hide();
    // activate new
    var newButton = this.button(active).addClass('active');
    this.tab(newButton).show();
  },
  "li click" : function(el, ev){
    // prevent the default setting
    ev.preventDefault();
    // update the route data
    can.route.attr(this.options.attr, this.tab(el)[0].id)
  }
});

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

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

// adds the controller to the element
new HistoryTabs( '#components',{attr: 'component'});
new HistoryTabs( '#people',{attr: 'person'});

How can we define route?

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:

What is can.route.delegate?

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
}
<a href="#!login">Login</a>
<a href="#!">Logout</a>

<ul id='modules' style="display:none">
    <li id='login'><a href="#!login">Login</a></li>
    <li id='filemanager'><a href="#!filemanager">Filemanager</a></li>
    <li id='contacts'><a href="#!contacts">Contacts</a></li>
</ul>

var Navigate = can.Control({
    "{can.route} module add" : function(){
       this.element.show();
    },
    "{can.route} module set" : function(route, ev, newVal,oldVal){
       $("#"+oldVal).removeClass("active");
       $("#"+newVal).addClass("active");
    },
    "{can.route} module remove" : function(){
       this.element.hide();
        this.element.children().removeClass("active");
    }
});

can.route(":module");

new Navigate("#modules");
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License