Friday, January 11, 2013

Backbone.occamsrazor.js

Despite of the awful name this is one of the nicest experiments I ever done (at least in Javascript :-) ).
I used occamsrazor.js to enable heterogeneous collections in backbone.js.

What ?

Backbone.js api is based on a RESTful architecture. This architecture is based on "resources" and "verbs". Resources are atomic unit of information identified by URLS. Verbs are an unified interface to make basic operations on resources (GET, PUT, POST, DELETE). 
3 of these VERBS operates on a single resource (GET, PUT and DELETE). POST operate on a special resource called "collection". A POST request on a collection causes the creation of a new resource. Backbone.js simplify the work allowing to issue a GET request to a collection. It is very useful to fetch all the models contained in a collection.
All of these client/server exchanges use basic JSON objects. These objects are just a bunch of attributes put together without a notions of the original model.
In order to transform this bunch of attributes in a full fledged object (with methods, prototype etc.) the backbone collection makes a simple assumption: all the object contained in a collection must be of the same type (model).

A positive side effect: models and views are now plugins!

occamsrazor.js enable collections formed by different models. As a (positive) side effect the models and model views are now plugins. I wrap the results in a simple backbone ehnancement.

Models and collections

Let's start with a simple empty collection:
    var shapesCollection = new Backbone.Occamsrazor.Collection;
With classic backbone collection you should define the model of the objects contained.
With Backbone.Occamsrazor.Collection instead you set what kinds of models it could contain. But first of all define some validator:
    var hasWidth = function (obj){
            if (obj instanceof Backbone.Model){
                return obj.has('width');
            }
            return 'width' in obj;
        },
        hasHeight = function (obj){
            if (obj instanceof Backbone.Model){
                return obj.has('height');
            }
            return 'height' in obj;
        },
        hasRadius = function (obj){
            if (obj instanceof Backbone.Model){
                return obj.has('radius');
            }
            return 'radius' in obj;
        },
        hasWidthHeight = occamsrazor.chain(hasWidth, hasHeight);
These validators take an object and returns a positive number if the object has a feature.
In this case is convenient validate both a simple object and a Backbone model.
You can find further explanations about validators in the occamsrazor.js documentation.
    shapesCollection.model.addConstructor(hasWidth, Backbone.Model.extend({
        getArea: function (){
            var w = this.get('width');
            return w*w;
        }
    }));

    shapesCollection.model.addConstructor(hasWidthHeight, Backbone.Model.extend({
        getArea: function (){
            var w = this.get('width'),
                h = this.get('height');
            return w*h;
        }
    }));

    shapesCollection.model.addConstructor(hasRadius, Backbone.Model.extend({
        getArea: function (){
            var r = this.get('radius');
            return Math.round(Math.PI * r * r);
        }
    }));
From now the collection can work with three kind of models transparently:
    shapesCollection.add([{width: 10, height: 5}, {width: 10}, {radius: 3}]);
    
    console.log(shapesCollection.at(0).getArea()); // 50
    console.log(shapesCollection.at(1).getArea()); // 100
    console.log(shapesCollection.at(2).getArea()); // 28

Views

If you use an heterogeneus collection you will surely need something analogous for the views.
You will probably need a different view for each model. This works almost the same as models. First create a collection view::
    var shapesView = new Backbone.Occamsrazor.CollectionView({collection: shapesCollection, el: $('#myid')});
And then add the views:
    shapesView.itemView.addConstructor([null, hasRadius], Backbone.Occamsrazor.ItemView.extend({
        tagName:  'div',
        render: function (){
            this.$el.html('The area of the circle is ' + this.model.getArea());
            return this;
            
        }
    }));

    shapesView.itemView.addConstructor([null, hasWidth], Backbone.Occamsrazor.ItemView.extend({
        tagName:  'div',
        render: function (){
            this.$el.html('The area of the square is ' + this.model.getArea());
            return this;
        }
    }));

    shapesView.itemView.addConstructor([null, hasWidthHeight], Backbone.Occamsrazor.ItemView.extend({
        tagName:  'div',
        render: function (){
            this.$el.html('The area of the rectangle is ' + this.model.getArea());
            return this;
        }
    }));
You should notice that I used Backbone.Occamsrazor.ItemView as view constructor function. This is nearly identical to Backbone.View: the only difference is in the way the model is passed to the costructor.
This emphasizes the fact that you must pass the model as argument and allows occamsrazor to pick the right view for a that model.

I have prepared a simple demostration of the potential of this library here. This is the classic todomvc application by Addy Osmani. The interesting part is in how it is simple adding plugins to enhance the application without touching the original code.

I hope this makes clear how occamsrazor.js can be helpful !