BackboneMVC

Download backbone-mvc.js View on GitHub

BackboneMVC adds the missing Controller component to your project if you use Backbone.js, giving you a complete MVC framework. Like CakePHP, it automatically routes your urls to the correct controller and action, but it is a framework for the web front end.

Introduction

Backbone.js does an excellent job of keeping the HTML UI and your Javascript business in sync. However the difficulties we face when we build Javascript web applications are not completely addressed by Backbone.js. Sometimes the difficulty of efficiently organizing the Javascript logic itself is prominent. Using urls to organize the execution of the Javascript logic is a good way to implement your business logic. The browsers' assumption of a traditional server/client model is not compromised, so the browsers' basic functions can be preserved. It also helps resume the correct stage in your business flow from a stateless page.

Backbone.js provides the Router component to map a url to a function. It becomes cumbersome when the application is divided into many modules and there are many operations to be mapped for different urls. Besides, due to the limitations of the language itself, it is not easy to share common processes (like pre-processors and post-processors) among those operations without a lot of code duplication. It will become even verbose if your application has a lot asynchronous calls.

BackboneMVC is designed to address just these issues.

Now you don't have to worry about defining the Router. BackboneMVC takes care of this chore for you automatically. Just define the Controllers and it starts to work automatically. You also get the functionality of automatic session checking and event hooks like 'beforeFilter' and 'afterRender'. There are also some Backbone.js helper utilities provided as well.

BackboneMVC doesn't accomplish what Backbone cannot achieve, but it makes many tasks easier by providing useful shortcuts. So it is very practical and useful.

Getting started

BackboneMVC depends on some other libraries. Before importing it, jQuery, Underscore and Backbone.js must be imported.

<!DOCTYPE html>
<html>
    <head>
        <script src="http://code.jquery.com/jquery-1.7.2.min.js" 
		type="text/javascript" language="JavaScript" ></script>
        <script src="http://underscorejs.org/underscore-min.js" 
		type="text/javascript" language="JavaScript" ></script>
        <script src="http://backbonejs.org/backbone-min.js" 
		type="text/javascript" language="JavaScript" ></script>
        <script src="backbone-mvc.js" type="text/javascript" 
		language="JavaScript" ></script>
    </head>

...
</html>

Then you can start to define your controllers. It follows the same syntax as when you define Backbone Models and Views. To define a Controller, use the BackboneMVC.Controller.extend(properties) method :

    var Controller1 = BackboneMVC.Controller.extend({
        name: 'ctrl1', /* the only mandatory field */

        /**
         * This is a standard action method, it is invoked
         * automatically if url matches
         */
        hello: function(){
            alert('Hello world!');
        },

        helloInChinese: function(){
            //you can invoke any method in this controller (including the private methods for sure)
            this._privateMethod();
        },

        /**
         * This function will remain untouched, the router cannot see
         * this method
         */
        _privateMethod: function(){
            alert('你好世界!');
        }
    })
Notice:
  • You can put the attributes and methods for your Controller in the properties parameter.
  • You must at least specify the name attribute, so the router will know what the controller's name in the url is.
  • The methods that do not start with an underscore(_) are action methods, and will be mapped by urls. The ones that start with an underscore are private methods, which will be disregarded by the router.

The last step is to create BackboneMVC's Router instance, and start Backbone's History component, so the routing mechanism will be activated.


    var router = new BackboneMVC.Router(); //Start the new automatic router
    Backbone.history.start(); //We still call Backbone's default component here

That's all. Give it a try. (Be aware of the URL change in your browser.)

Trigger hello() : #ctrl1/hello

Trigger helloInChinese() : #ctrl1/helloInChinese

Notice:
  • You only need to define the Controller, you don't even have to instantiate it.
  • If you directly visit that URL(with the hash tag), the method will still be invoked.

Features

URL-Action Mapping

According to Backbone's Router component, the hash fragments (#page) or a portion of the static url can be used for routing. It's the same for BackboneMVC's routing strategy. In this documentation, we will assume you use the hash fragment method.

Like CakePHP, the hash fragments are divided by slashes into three parts: controller name, method and optional parameters.

 URL_TO_ROUTE := CONTROLLER_NAME '/' METHOD_NAME (ADDITIONAL_PARAMETERS)
 ADDITIONAL_PARAMETERS := '/' LITERAL_VALUE (ADDITIONAL_PARAMETERS )
 CONTROLLER_NAME := LITERAL_VALUE
 METHOD_NAME : LITERAL_VALUE
 LITERAL_VALUE: [^/]+
            
Examples:
  • controller1/method1
  • my_controller/my-method/happy/birthday

The optional parameters can be 0 or many, which are also separated by slash(/). No matter how many parameters are in the url, they will all be tossed over to the method in the order of appearance.

BackboneMVC.Controller.extend({
    name: 'my_controller', /* the only mandatory field */

    'my-method': function(how, when){
        var phrase = how + ' ' + (when || 'unknown');
        this._output(phrase);
    },

    _output: function(string){
        $('#area1').append($('<div>' + string + '</div>'));
    }
})

Try it in action:

Trigger my-method() with 2 parameters:
#my_controller/my-method/happy/birthday

Trigger my-method() with 1 parameter:
#my_controller/my-method/happy

Private methods can't be triggered. This helps maintain your encapsulation.

Trigger a private method (this will fail): my_controller/_output/really?

Asynchronous Calling

The navigate() method of Backbone's Router component can trigger url routing programmatically. BackboneMVC further enhances this method, and the method now returns a JQuery Deferred object. You can use it to make sure the action method has finished. This is useful when making asynchronous calls.

See the following example:

var AsynchronousController = BackboneMVC.Controller.extend({
    name: 'asynchronous', /* the only mandatory field */

    'method': function(){
        var deferred = new $.Deferred();

        var colors = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple'];
        var index = 0;

        var instance = this;
        (function op(){
            if(index < colors.length){
                instance._changeColor(colors[index++]);
                setTimeout(op, 567);
            }else{
                deferred.resolve();
            }
        })();
        return deferred;
    },

    _changeColor: function(color){
        $('#area2').css('background-color', color);
    }
});

function procedure1(){
    var r = router.navigate('asynchronous/method', {trigger:true, replace: false});
    r.done(function(){
        $('#area2').html('Nice I change to white!');
        (new AsynchronousController())._changeColor('white');
    });
};

         

See it in action:

Trigger the procedure1() method:
Click here to trigger procedure1()

Event Hooks

Like CakePHP, before and after invoking an action method, beforeFilter and afterRender event handlers can also be set.

For example:

 BackboneMVC.Controller.extend({
    name: 'event_hooks', /* the only mandatory field */

    beforeFilter: function(){
        this._report('beforeFilter invoked');
    },

    method1: function(){
        this._report('method1 invoked');
    },

    method2: function(){
        var index = 0;
        var instance = this;
        var deferred = new $.Deferred();
        (function op(){
            if(index ++ < 5){
                instance._report(index);
                setTimeout(op, 345);
            }else{
                deferred.resolve();
            }
        })();
        return deferred;
    },

    afterRender: function(){
        this._report('afterRender invoked');
    },

    _report: function(text){
        $('#area3').append('<div>' + text + '</div>');
    }
});
 

Trigger mehtod1() #event_hooks/method1

If your action method return a Deferred object, then the afterRender event hook will be deferred in execution

Trigger method2() #event_hooks/method2

Rules for the call chain of beforeFilter, afterRender and the action method
  • All of the beforeFilter, afterRender and the action method can opt to return a Deferred object. If either of them does, then its successor will wait on the predecessor's Deferred object. Unless the Deferred object is resolved, all the subsequent methods on the chain won't be executed.
  • If a predecessor return a non-deferred value, and the value can be evaluated to true, the successor will run as it does normally, or otherwise the call chain will be interrupted.
  • If a predecessor doesn't return a value, the successor will run as it does normally.
  • If at any point, the call chain is interrupted, either by a rejected object or a false value, and the navigate() method is used to issue the call, the returned Deferred object from navigate() will be rejected.

See the following example:

var EventHooks1= BackboneMVC.Controller.extend({
    name: 'event_hooks1', /* the only mandatory field */

    beforeFilter: function(){
        this._report('beforeFilter invoked');
        //always successful (can also be achieved if return nothing)
        return true;
    },

    method1: function(){
        this._report('method1 invoked');
        var deferred = new $.Deferred();

        var colors = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple'];
        var index = 0;

        var instance = this;
        (function op(){
            if(index < colors.length){
                instance._changeColor(colors[index++]);
                setTimeout(op, 234);
            }else{
                deferred.resolve();
            }
        })();
        return deferred;
    },

    method2: function(){
        this._changeColor('MidnightBlue');
        this._report('method2 invoked');
        this._report('afterRender won\'t be executed');
        return false; // a false value, the subsequent method has no chance to be executed
    },

    method3: function(){
        this._changeColor('Indigo');
        this._report('method3 invoked');
        this._report('afterRender will be executed');
        // return nothing has the same effect as returning true
    },

    afterRender: function(){
        // reject, so the call chain will eventually fail even though the
        // main action method is executed
        this._report('afterRender invoked');
        return (new $.Deferred()).reject();
    },

    _report: function(text){
        $('#area4').append('<div>' + text + '</div>');
    },

    _changeColor: function(color){
        $('#area4').css('backgroundColor', color);
    }
});

window['procedure2'] = function(){
    var r = router.navigate('event_hooks1/method2', {trigger:true, replace: false});
    r.done(function(){
        (new EventHooks1())._report('procedure2 successful');
    }).fail(function(){
        (new EventHooks1())._report('procedure2 failed');
    });
};

window['procedure3'] = function(){
    var r = router.navigate('event_hooks1/method3', {trigger:true, replace: false});
    r.done(function(){
        (new EventHooks1())._report('procedure3 successful');
    }).fail(function(){
        (new EventHooks1())._report('procedure3 failed');
    });
};

Trigger method1() #event_hooks1/method1

Trigger procedure2() procedure2()

Trigger procedure3() procedure3()

When used with Deferred objects, the event hooks can be useful if you want to prepare templates for your actions to render views, or select the corresponding navigation entry for all the actions in the controller after their execution.

Session Checking

Similar to beforeFilter and afterRender methods, you can also define a checkSession method. Then all action methods with a prefix of 'user_' in their names, will be invoked only after the checkSession is called.

If checkSession returns false or a Deferred object, which will later be rejected, the action methods will not be invoked.

See example:

BackboneMVC.Controller.extend({
    name: 'session_enabled', /* the only mandatory field */

    beforeFilter: function(){
        this._report('beforeFilter invoked');
        return true;
    },

    checkSession: function(){
        this._report('checkSession invoked');

        var deferred = new $.Deferred();

        var instance = this;
        setTimeout(function(){
            instance._report('session is valid!');
            deferred.resolve();
        }, 2000);
        return deferred;
    },

    user_method1: function(){
        this._report('method1 invoked');
    },

    user_method2: function(){
        this._report('secure method2 invoked');
        this._changeColor('green');
    },

    method2: function(){
        this._report('normal method2 invoked');
        this._changeColor('DimGray');
    },

    _report: function(text){
        $('#area5').append('<div>' + text + '</div>');
    },

    _changeColor: function(color){
        $('#area5').css('backgroundColor', color);
    }
});

Trigger user_method1() #session_enabled/user_method1

You can even omit the 'user_' prefix:

Trigger method1() #session_enabled/method1

However if the method without the 'user_' prefix is already defined, then the shortcut will not overwrite the existing one, see example:

Trigger user_method2() #session_enabled/user_method2

Trigger method2() #session_enabled/method2

As you can see, the checkSession is invoked after beforeFilter, but before the action method.

Controllers Are Singletons

Controllers are all singletons. So no matter when and where you instantiate a controller, the instance always keeps the same state.

var Singleton = BackboneMVC.Controller.extend({
    name: 'singleton', /* the only mandatory field */
    value: 0,

    method: function(){
        this._report(this.value++);
    },

    _report: function(text){
        $('#area6').append('
' + text + '
'); } }); window['procedure4'] = function(){ (new Singleton()).method(); };

Trigger method() #singleton/method

Call procedure4() procedure4()

Controller Inheritance

Controller.extend() can be used to do class inheritance. So your controllers are able to share functionality from their parent controller.

See example:

var Parent = BackboneMVC.Controller.extend({
    name: 'parent', /* the only mandatory field, even though the parent is not planned to be used,
    it will still need to be assigned a name. */

    method: function(){
        this._report('Parent method invoked');
        this._changeColor('#141F2E');
    },

    _report: function(text){
        $('#area7').append('<div>' + text + '</div>');
    },

    _changeColor: function(color){
        $('#area7').css('backgroundColor', color);
    }
});

var Child1 = Parent.extend({
    name: 'child1', /* the only mandatory field */

    method: function(){
        this._report('Child1 method invoked');
        this._changeColor('green');
    }
});

var Child2 = Parent.extend({
    name: 'child2' /* the only mandatory field */

    //this controller doesn't implement anything, so its parent's methods will be passed over.
});

Trigger Child1::method() #child1/method

Trigger Child2::method() #child2/method

Custom Routing Rules

When BackboneMVC's automatic routing doesn't suffice for your requirements, customized routing rules can be used to comply with backbone.js' convention. This can be achieved by further extending the BackboneMVC.Router class.

    var MyExtendedRouter = BackboneMVC.Router.extend({
        routes:{
            '' : 'index',
            'root/action/hardcoded_value': 'special_handling'
        },

        index: function(){
            // do customized logic, for example, launch a default controller's action
            router.navigate("root/index", {trigger:true, replace: true});
        },

        special_handling: function(){
            (new RootController())._report(
                "I just thought of something more important to do.";
            );

            (new RootController())._changeColor("purple");
        }
    });

    var RootController = BackboneMVC.Controller.extend({
        name: "root", /* the only mandatory field */

        index: function(){
            this._report("'index' method invoked");
            this._changeColor("blue");
        },

        action: function(param){
            this._report("'action' method triggered with " + param);
            this._changeColor("red");
        },

        _report: function(text){
            $("#area8").append("<div>' + text + '</div>");
        },

        _changeColor: function(color){
            $("#area8").css("backgroundColor", color);
        }
    });

Trigger routing for empty hash #

Some hash patterns that do not follow BackboneMVC's automatic routing can be dealt with this way, especially the empty hash which usually points to a default index page. The default controller's default action can be redirected to, as the example above shows, or any other code logic can be used.

If you define custom rules, they will always have higher priority than BackboneMVC's automatic routing. See examples:

Trigger special_handling() #root/action/hardcoded_value

Trigger RootController::method() #root/action/offer

Support or Contact

Have questions or suggestions? Please contact Changsi An by me@anchangsi.com .

Copyright 2012-2013 Changsi An