Tags

, , , , , , , ,

You are working on an enterprise application with each page being rendered with multiple ReSTfull services and have numerous views that you have to handle. If you look at your code you would realize that there are lots of duplicate codes in each view, how could we remove these duplicate codes? Let’s find out.

MasterView

A MasterView would be the main view in an application, and other view extends its features. If all the child view has certain common code to be executed, you could very well assign the task to MasterView to do that task. This will make the views on your project, maintainable, readable and clean.

For example, in my application I always render the view on collection.fetch success, so that I could ensure that my templates would be rendered only after loading the collection. So I would prefer to do this fetch code inside my view initialize() function. Now, I’m just talking about one view here and there are many sub views like this and the same fetch code has to be written to all these sub views and that’s a lot of code. So I could write this fetch code inside the MasterView so that the sub views will hookup with the MasterView to perform the function.

Let’s understand this with an example. Consider we are building an e-commerce application to sell mobile phones and tablets online. We have to views Phones and Tablets to list the items on display when the page loads.

Here is the Phones and Tablets view without using the MasterView concept.

Phones View

mobileStore.views.Phones = Backbone.View.extend({
    initialize: function() {
        var self = this;
        this.template = _.template(mobileStore.templateLoader.get('phones'));
        this.collection.fetch({
            success: function() {
                self.render();
            },
            error: function() {
                console.log('failed to load service');
            }
        });
    },
    render: function() {
        var phones = this.collection.toJSON();
        if (phones.status === 'success') {
            $(this.el).html(this.template({'data': phones.data[0]}));
        }
        else{
            console.log('no data found');
            $(this.el).hide();
        }
    }
});

Tablets View

mobileStore.views.Tablets = Backbone.View.extend({
    initialize: function() {
        var self = this;
        this.template = _.template(mobileStore.templateLoader.get('tablets'));
        this.collection.fetch({
            success: function() {
                self.render();
            },
            error: function() {
                console.log('failed to load service');
            }
        });
    },
    render: function() {
        var tablets = this.collection.toJSON();
        if (tablets.status === 'success') {
            $(this.el).html(this.template({'data': tablets.data[0]}));
        }
        else{
            console.log('no data found');
            $(this.el).hide();
        }
    }
});

In both the views you could identify a lot of common code inside the initialize block and inside render. I’m using the Jsend specification for JSON response, which would be of this format,

{
    status: "success",
    data: [
        {
            
        }
    ]
}

We have to render the template only if the service returns status ‘success’ else no need to render the view. Jsend specification for JSON response is quite useful while working on enterprise application with a big team because it under lays a common JSON response contract across the team and the structure itself speak about itself. Here in this sample I’m only checking the status, you could also check the size of the data node because there are chances that the status would be success but there will not be any data returned from server.

I prefer to externalize my templates, so that I could load them when needed. The code mobileStore.templateLoader get the template pre loaded before loading the view. By this way you could keep the DOM away from script tags. For more information on external template loading please visit Christophe Coenraets blog, where he has specified some of the best practices while creating a Backbone applications.

Once the MasterView concept is applied, how would these two view looks like? Here they are,

Phones View Modified

mobileStore.views.Phones = mobileStore.views.MasterView.extend({
    initialize: function() {
        this.template = _.template(mobileStore.templateLoader.get('phones'));
        mobileStore.views.MasterView.prototype.initialize.call(this);
    },
    load: function() {
        mobileStore.views.MasterView.prototype.load.call(this);
    },
    render: function(data) {
        $(this.el).html(this.template({'phones': data[0]}));
    }
});

Tablets View Modified

mobileStore.views.Tablets = mobileStore.views.MasterView.extend({
    initialize: function() {
        this.template = _.template(mobileStore.templateLoader.get('tablets'));
        mobileStore.views.MasterView.prototype.initialize.call(this);
    },
    load: function() {
        mobileStore.views.MasterView.prototype.load.call(this);
    },
    render: function(data) {
        $(this.el).html(this.template({'tablets': data[0]}));
    }
}); 

Does that look more compact that the previous one and more readable?

Let’s see what’s happening inside our MasterView,

mobileStore.views.MasterView = Backbone.View.extend({
    activeConnections: 0,
    initialize: function() {
        _.bindAll(this);
        this.collection.bind('change', this.successHandler);
    },
    load: function(params) {
        this.activeConnections++;
        this.collection.fetch({
            reset: true,
            data: $.param(params),
            success: this.successHandler,
            error: this.errorHandler
        });
    },
    successHandler: function(result) {
        this.activeConnections--;
        var self = this;
        var responseData = result.toJSON();
        if (responseData.status === "success" && responseData.data.length > 0) {
            self.render(responseData.data[0]);
        }
        else {
            $(self.el).hide();
        }

        if (this.activeConnections === 0) {
            this.onRenderComplete();
        }
    },
    errorHandler: function() {
        this.activeConnections--;
        if (this.activeConnections === 0) {
            this.onRenderComplete();
        }
        console.log("MasterView:ErrorHandler");
    },
    onRenderComplete: function() {
        console.log('page has finished rendering');
    }
});

Okay, let’s analyze the MasterView in detail.

Initialize method
When the initialize method of MasterView is being accessed by child view, first we use the _.bindAll(this) followed by the change event for the collection, of the child view. On change in collection, it renders the method successHandler of the MasterView.

Load method
When the child view calls the method load(), it does the collection fetch. The load method could have a param which is the optional request params for the service. On successful fetch it calls the successHandler and on failure it calls the errorHandler.

SuccessHandler
successHandler is the method invoked on fetch collection complete. Inside this method, we get the collection, parse it with method .toJSON() and then check the service status is success. If success, it will invoke the render method of the child view and the processed data from the collection, is supplied to the render method as input. The child view render then bind the template with the data provided.

ErrorHandler
errorHandler is the method to handle error scenarios while fetching collection.

OnRenderComplete
onRenderComplete is a custom function and it’s an optional feature. This is my scenario, as I mentioned above the page is rendered using multiple ReSTfull services, hence you might need to know when the page has finished loading, or may be you would like to log the time taken to load the card with all the services. For these function executions, we could use the onRenderComplete which is triggered when all the active connections hooked up with the view is completed.

Now, how do we render the Phones view and Tablets view? Here it is, on DOM ready execute the function. Following code snippet is based on our sample codes provided above.

$(function() {
    mobileStore.templateLoader.loadTemplate(['phones', 'tablets'], function() {
        var phones = new mobileStore.collections.Phones();
        var tablets = new mobileStore.collections.Tablets();
        
        var phonesView = new mobileStore.views.Phones({el:$('div#phonesContainer'),collection:phones});
        var tabsView = new mobileStore.views.Tablets({el:$('div#tabletsContainer'),collection:tablets});
        
        phonesView.load();
        tabsView.load();
    });
});

With the help of MasterView we could accomplish the following, while working with large applications

  1. Maintainable: The code is more optimized once we introduce the MasterView. All the common functionality taken care by the MasterView, and hence any change in code you just need to make the change with the MasterView.
  2. Reduced lines of code: Reduced lines of code will by default reduce the size of the .js files which reduces the page load time.
  3. Avoiding duplicate codes: This is another aspect that we have to consider. I don’t like duplicate codes and while writing a function, I would try to make as generic as possible, to make it reusable. This in-turn results in code maintainability and reduced lines of code.

Yes, we are done! That’s it from me about MasterView. Hope you would give it a try with the MasterView on your Backbone application.

Cheers!

Advertisements