CanJS - Route

canjs
canjs-route2

http://blog.bitovi.com/routing-in-canjs/ - done reading
http://bitovi.com/blog/2012/05/hashchange-routing-can-route-1.html - done reading
http://bitovi.com/blog/2012/05/hashchange-routing-can-route-2.html - done reading
http://blog.bitovi.com/hashchange-routing-with-can-route-part-1-basics/ - done reading
http://blog.bitovi.com/hashchange-routing-with-can-route-part-2-advanced/ - done reading
http://canjs.com/docs/can.route.pushstate.html - done reading

What is can.route?

can.route is a special observable that updates and responds to changes in window.location.hash. Routing in CanJS allows us to manage browser history and client state by synchronizing the window.location.hash with the AppState object, which is a can.Map observable object. In other words, we can use routing to reflect the state of our application or set the state of our application. One of the things that makes routing powerful is that it records the state of the application in the browser’s history.

To support the browser's back button and bookmarking in an Ajax application, most applications use the window.location.hash. By changing the hash (via a link or JavaScript), one is able to add to the browser's history without changing the page. can.route is a can.Map that represents the window.location.hash as an object. For example, if the hash looks like:

#!type=videos&id=5

the data in can.route looks like:

{ type: 'videos', id: 5 }

Does can.route support the browser's history / back button?

Yes.

How does single-page browser history stuff work without using CanJS?

This done using the hashchange event. 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 via JavaScript:

window.location.hash = "foo/bar"

or changed when the user clicks on a link like:

<a href="#recipe/5">Show</a>

You can listen to for the hashchange event:

window.addEventListener('hashchange', function(){
  console.log('the hash has changed')
})

and do whatever you like, such as updating the page.

Why should we support the back and forward buttons if we have a "single page" application / site?

The advantage of having a single page application is that everything is loaded in one shot, and the browser does not need to re-load or re-parse the resources on subsequent pages, and thus improve performance. However, if a significant portion of the page is changed, the user may perceive it as a new page, and may use the back button if he need to see the previous screen, and unfortunately, clicking on the back button would take the user to another page, the page he visited before accessing your application. Clicking the forward button again will take the user back to the home screen of your application which is not the screen that the user was at before he click the back button.

What are the 3 basic steps to setup routing?

  1. define the possible routes by calling can.route,
  2. bind our appState object to the route with a call to can.route.map
  3. call can.route.ready(), which sets up two-way binding between the browser’s window.location.hash and the can.route’s internal can.Map.

Since you already know about creating instances of can.Map, creating an appState object, which is a can.Map, will be easy. Let’s see how this works. Open up your app.js file and update it as shown below.

$(function () {
    var AppState = can.Map.extend({});
    var appState = new AppState();
    // Bind the application state to the root of the application
    $('#can-main').html(can.view('main.stache', appState));
    // Set up the routes
    can.route(':page', { page: 'home' });
    can.route(':page/:slug', { slug: null });
    can.route(':page/:slug/:action', { slug: null, action: null });
    $('body').on('click', 'a[href="javascript://"]', function(ev) {
        ev.preventDefault();
    });
    // Bind the application state to the can.route
    can.route.map(appState);
    can.route.ready();
    //appState.attr('page', 'restaurants');
    appState.bind('change', function(ev, prop, change, newVal, oldVal) {
        alert('Changed the “' + prop + '” property from “' + oldVal + '” to “' + newVal + '”.');
    });
});
var AppViewModel = can.Map.extend({
    define: {
        page: {}
        color: {}
    }
});
 
// Load the pushstate plugin 
import "can/route/pushstate"
 
// Import the AppViewModel
import AppViewModel from 'app-view-model';
 
// Create an instance of AppState (appState)
var appViewModel = new AppViewModel({});
 
// Make appState the route's internal can.Map
can.route.map(appViewModel);
 
// Each element that will be set on the app-state must be preceded by a colon
// Also, set a default value for page (the login page)
can.route(':page');
can.route(':page/:color');
 
// Initialize routing
can.route.ready();
 
// Render the base application
// Link appState to index.stache
$('#app').html(index(appViewModel));
 
appViewModel.attr('page', 'login');

How can we define a route?

can.route(':page', { page: 'home' });
can.route(':page/:slug', { slug: null });
can.route(':page/:slug/:action', { slug: null, action: null });

The first line does two things. It creates a base route that is bound to one property 'page' in our appState object, and sets the default value of the page property to 'home'.

The second line does two things. It binds a new slug property (a child property of the 'page' property), and sets the default value of the slug property to null.

This line does two things. It binds a new action property to our appState object, and sets the default value of the action property to null.

In our app, this will allow the following URLs:

  1. #! (which will set page to 'home' because that’s the default)
  2. #!orders/ (the page variable is 'orders')
  3. #!restaurants/ (the page variable is 'restaurants')
  4. #!restaurants/spago/ (the page variable is 'restaurants' and the slug variable is 'spago')
  5. #!restaurants/spago/order/ (page: 'restaurants', slug: 'spago', action: 'order')

Let’s take a moment to see how these routes are bound to our appState object. Notice the //appState.attr('page', 'restaurants'); line at the end of our app.js file; let’s uncomment that line so it looks like appState.attr('page', 'restaurants');. Now, refresh the app in your browser. The path will now be #!restaurants, and you’ll notice that the Restaurants link in the navigation is highlighted.

Note that, after we initialized our routes, updating the value of our appState’s page property caused the route to update as well. The value of the page property was serialized and appended to the window.location.hash.

How can we register a function that get run when the URL is changed? Does CanJS offer a different way so that appropriate view is automatically rendered? How can I use can.route to generate URL for a given resource? How does this routing on the front-end impact the back-end resources? When we need to fetch data from the back-end, what do we do? Is there a relationship or convention between the routing on the front-end and the routing on the back-end? Clearly, the route for the front-end should be different from the back-end because the front-end is a single page but the back-end is not really a page. If the route for the front-end is #!restaurants/spago, the route for the back-end can be just /restaurants/spago without the #!.

How can we listen for changes in the hash or can.route?

can.route keeps the state of the hash (browser hash) in-sync with the data contained within can.route. can.route is a can.Map. Understanding can.Map is essential for using can.route correctly.

You can listen to changes in an Observe with bind(eventName, handler(ev, args…)) and change can.route's properties with attr. Listen to changes in history by binding to changes in can.route like:

can.route.bind('change', function(ev, attr, how, newVal, oldVal) {
})
  • attr - the name of the changed attribute
  • how - the type of Observe change event (add, set or remove)
  • newVal/oldVal - the new and old values of the attribute

How can we change the hash via code?

We can changes the data with attr like:

can.route.attr('type','images');

Or we can change multiple properties at once like:

can.route.attr({type: 'pages', id: 5}, true)

When you make changes to can.route, they will automatically change the hash, and when the URL is change, the data in can.route is automatically updated.

How can we use can.route with old browser that do not support the hashchange event?

If you are using jQuery, you can include Ben Alman's HashChange Plugin to support the event in the unsupported browser(s).

How can we listen for can.route events inside a can.Control?

Using templated event handlers, it is possible to listen to changes to can.route within can.Control. This is convenient as it allows the control to listen to and make changes whenever the route is modified, even outside of the control itself.

// create the route
can.route("#!content/:type")
can.Control({
    // the route has changed
    "{can.route} change": function(ev, attr, how, newVal, oldVal) {
        if (attr === "type") {
            // the route has a type
        }
    }
});

How can we create and bind routes with can.Control.route?

Using can.Control.route, a builtin plugin to CanJS, cuts down on the amount of code needed to work with can.route in can.Control. With this plugin, it is possible to both create routes and bind to can.route at the same time. Instead of creating several routes to handle changes to type and id, write something like this in a control:

can.Control({
    // the route is empty
    "route": function(data) {
    },
    // the route has a type
    ":type route": function(data) {
    }, 
    // the route has a type and id
    ":type/:id route": function(data) {
    }
});

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);

In the above code, we created a can.Control object named Router, but I supposed that we can give it any name we want, and any can.Control object can listen route events.

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>

To listen to an empty hash (""), "#", or "#!", you can simply write "route" like:

Router = can.Control(
    {
        "route" : function(){
            console.log("empty hash")
        }
    }
)
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 );
                });
        }
    }
);

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 use the can.Map.delegate plugin?

Sometimes, you might only want to trigger a function when the route changes only once, even if the route change gets called multiple times. By using the delegate plugin, this is extremely easy. This plugin allows you to listen to change, set, add, and remove on can.route.

If you wanted to, say, show a list of recipes when type was set to recipe and show a specific recipe when id was set, you could do something like:

can.Control({
    "{can.route} type=recipe set": function( ev, prop, how, newVal, oldVal ) {
        // show list of recipes
    },
    "recipe/:id": function(data) {
        // show a single recipe
    }
});

If we didn't only listen to when recipe is set, then every time we chose to show a single recipe, we would create and show the list of recipes again which would not be very efficient.

What is a route template?

The method signature is: can.route(template, [defaults]). template is a string that maps path section values to property values on the AppViewModel instance.

can.route(":page/:color")

This pattern matches a URL with two path sections. If this route is matched, page and color will be set on the AppViewModel.

Can route be made with path section?

Routes can also be made with un-mapped path sections:

can.route("page/:color")

Note the absence of the colon preceding page. This route will only update the color property of the AppViewModel (only color has the colon preceding it).

In summary, the can.route method takes a URL fragment string as a parameter. Path sections preceded by a colon link to properties on the AppViewModel instance and can.route

What is the purpose of the pushstate plugin?

Use path instead of hash.

How can we use the pushstate plugin?

The pushstate plugin uses the same API as can.route. To start using pushstate plugin all you need is to load can/route/pushstate, it will set itself as default binding on can.route. You can check current binding by inspecting can.route.currentBinding, the default value is "hashchange".

What is the purpose of the can.route.current function?

The can.route.current function takes one parameter, an object, and check to see if its data match the current route / URL.

can.route.attr('id', 5) // location.hash -> "#!id=5"
can.route.current({ id: 5 }) // -> true
can.route.current({ id: 5, type: 'videos' }) // -> false
can.route.attr('type', 'videos')
// location.hash -> #!id=5&type=videos
can.route.current({ id: 5, type: 'videos' }) // -> true

What is the purpose of the can.route.deparam function?

Extract data from a given route / URL.

can.route.deparam( url )

The can.route.deparam function extract data from the given URL and return the object containing the data extracted.

can.route.deparam("id=5&type=videos");

It's important to make sure the hash or exclamantion point is not passed to can.route.deparam otherwise it will be included in the first property's name.

can.route.attr("id", 5) // location.hash -> #!id=5
can.route.attr("type", "videos")
// location.hash -> #!id=5&type=videos
can.route.deparam(location.hash)
// -> { #!id: 5, type: "videos" }

can.route.deparam will try and find a matching route and, if it does, will deconstruct the URL and parse our the key/value parameters into the data object.

can.route(":type/:id")
can.route.deparam("videos/5");
// -> { id: 5, route: ":type/:id", type: "videos" }

Why do we have to invoke can.route.map before invoking can.route.bind or do any binding to can.route?

Call can.route.map at the start of the application lifecycle, before any calls to can.route.bind. This is because can.route.map creates a new internal can.Map, replacing the default can.Map instance.

What is the purpose of the can.route.param function?

Get a route path from given data.

can.route.param( data )

The can.route.param function returns a string, the route, with the data populated in it.

can.route.param( { type: "video", id: 5 } )
// -> "type=video&id=5"

If a route matching the provided data is found, that URL is built from the data. Any remaining data is added at the end of the URL as & separated key/value parameters.

can.route(":type/:id")
can.route.param( { type: "video", id: 5 } ) // -> "video/5"
can.route.param( { type: "video", id: 5, isNew: false } )
// -> "video/5&isNew=false"
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License