Thursday, June 19, 2014

Cleanup Events With Backbone Routers

Backbone routers allow you to listen to route events. These are triggered when one of the defined paths in the router matches the URL. For example, the paths of your application can be statically defined and named in the routes object. The route events triggered by the router use the route name — route:home, or route:help. What's nice about this is that different components can subscribe to router events. This means I don't have to define all my route handling code in one place — each component of my application is able to encapsulate it's own routing logic. One piece of the puzzle that's missing is cleaning up my views once the route that created them is no longer active.

Here's an example of the kind of routing code I'd like to write. A handler for the route, when it becomes active, and a handler for when the route changes to something else.

// Respond to "page1".
app.on( 'route:page1', function() {
    $( 'a[href="#page1"]' )
        .css( 'font-weight', 'bold' );
});

// Cleanup "page1" when the route changes.
app.on( 'cleanup:page1', function() {
    $( 'a[href="#page1"]' )
        .css( 'font-weight', 'normal' );
});

// Respond to "page2".
app.on( 'route:page2', function() {
    $( 'a[href="#page2"]' )
        .css( 'font-weight', 'bold' );
});

// Cleanup "page2" when the route changes.
app.on( 'cleanup:page2', function() {
    $( 'a[href="#page2"]' )
        .css( 'font-weight', 'normal' );
});

$(function() {
    Backbone.history.start();    
});

Here, the route events are what's triggered when something get's activated. The cleanup events trigger when something get's deactivated. Here's an example of how to extend the router itself in order to trigger these cleanup events.

var Router = Backbone.Router,
    proto = Router.prototype,
    app;

app = new ( Router.extend({
    
    routes: {
        'page1': 'page1',
        'page2': 'page2',
        '*path': 'default'
    },
    
    trigger: function() {
        
        var args = Array.prototype.slice.call(
                arguments ),
            result = ( /^route:(\w*)$/ ).exec( args[ 0 ] );
        
        if ( result ) {
            this.cleanup();
            this.prevArgs = [ 'cleanup:' + result[ 1 ] ]
                .concat( args.slice( 1 ) );
        }
        
        return proto.trigger.apply( this, args );
    
    },
    
    cleanup: function() {
        if ( this.prevArgs ) {
            proto.trigger.apply( this, this.prevArgs );
        }
    }
    
}))();

All we're doing with this code is providing a custom implementation of the trigger() method. In it, we check if the router is triggering a route a event, and if so, we try to trigger a cleanup event first. Then we just need to store the new route arguments so that we can trigger another cleanup event down the line.