Defining child views without an initialize method

I have a large number of views (more than 50) which all extend from a single abstract base view, and therefore have a similar layout and many other features in common (event handlers, some custom methods and properties, etc).

I am presently using the initialize method of my base view to define the layout, which involves a subview, somewhat like the following:

App.BaseView = Backbone.View.extend({

  //...

  initialize: function() {
    this.subView = new App.SubView();
  },

  render: function() {
    this.$el.html(this.template(this.model.toJSON()));
    this.subView.$el = this.$('#subview-container');
    this.subView.render();
    return this;
  },

  //...

});

I find, however, that for many views which extend my base view I need to override the initialize method, which calls the base class initialize (I also extend my events hash quite often as well). I dislike having to do this, especially with so many views which extend the base class.

In this post from a Backbone Github repository issue Derick Bailey says:

I'm also not a fan of requiring extending classes to call super methods for something like initialize. This method is so basic and so fundamental to any object that extends from a Backbone construct. It should never be implemented by a base type - a type that is built with the explicit intent of never being instantiated directly, but always extended from.

So on this model I should be able to have an initialize available for each inheriting view class. This makes perfect sense to me; but how can I then implement the kind of general layout I need for my inheriting views? In the constructor method?

I don't know if what I want might be possible out-of-the-box with something like Marionette or LayoutManager, both of which I've briefly looked at but never used, but I would much prefer doing this in vanilla Backbone at the moment.

Answers:

Answer

Where to implement the initializing of the base class?

The way I like to do it is to initialize base classes in the constructor leaving the initialize function empty. It makes sense as the initialize function is only a convenience offered by Backbone and is really just an extension of the constructor.

In fact, Backbone do this a lot. Most if not all functions and properties that we override often are there only to be easily overriden.

Here's a quick list of such example:

  • Model: initialize, defaults, idAttribute, validate, urlRoot, parse, etc.
  • Collection: initialize, url, model, modelId, comparator, parse, etc.
  • View: initialize, attributes, el, template, render, events, className, id, etc.

These functions are left to the user to implement his own behaviors and to keep that useful pattern in a base class, they should be kept untouched and the base class behavior should be hooked into other functions if possible.

Sometimes, it can get difficult, like if you want to do something before initialize is called in the constructor, but after the element and other properties are set. In this case, overriding _ensureElement (line 1223) could be a possible hook.

_ensureElement: function() {
    // hook before the element is set correctly
    App.BaseView.__super__._ensureElement.apply(this, arguments);
    // hook just before the initialize is called.
}

This was just an example, and there are almost always a way to get what you want in the base class without overriding a function that the child will also override.


Simple base class

If the base view is used in a small component and overriden by few child views, and mostly used by the same programmer, the following base view could be enough. Use Underscore's _.defaults and _.extend to merge the child class properties with the base class.

App.BaseView = Backbone.View.extend({

    events: {
        // default events
    },

    constructor: function(opt) {
        var proto = App.BaseView.prototype;

        // extend child class events with the default if not already defined
        this.events = _.defaults({}, this.events, proto.events);

        // Base class specifics
        this.subView = new App.SubView();

        // then Backbone's default behavior, which includes calling initialize.
        Backbone.View.apply(this, arguments);
    },

    render: function() {
        this.$el.html(this.template(this.model.toJSON()));

        // don't set `$el` directly, use `setElement`
        this.subView
            .setElement(this.$('#subview-container'))
            .render();

        // make it easy for child view to add their custom rendering.
        this.onRender();
        return this;
    },

    onRender: _.noop,

});

Don't set $el directly, use setElement instead.

Then a simple child view:

var ChildView = App.BaseView.extend({
    events: {
        // additional events
    },
    initialize: function(options) {
        // specific initialization
    },
    onRender: function() {
        // additional rendering
    }
});

Advanced base class

If you're facing one of the following situation:

  • overriding render is problematic, don't like onRender
  • the events property (or any other property) is a function in the child or the parent or both
  • the programmer using the base class don't know about its specifics

Then it's possible to wrap the child properties implementation into new functions and Underscore's _.wrap function does just that.

App.BaseView = Backbone.View.extend({
    // works with object literal or function returning an object.
    events: function() {
        return { /* base events */ };
    },

    // wrapping function
    _events: function(events, parent) {
        var parentEvents = App.BaseView.prototype.events;
        if (_.isFunction(parentEvents)) parentEvents = parentEvents.call(this);
        if (parent) return parentEvents; // useful if you want the parent events only
        if (_.isFunction(events)) events = events.call(this);
        return _.extend({}, parentEvents, events);
    },

    constructor: function(opt) {
        var proto = App.BaseView.prototype;

        // wrap the child properties into the parent, so they are always available.
        this.events = _.wrap(this.events, this._events);
        this.render = _.wrap(this.render, proto.render);

        // Base class specifics
        this.subView = new App.SubView();

        // then Backbone's default behavior, which includes calling initialize.
        Backbone.View.apply(this, arguments);
    },

    /**
     * render now serves as both a wrapping function and the base render
     */
    render: function(childRender) {
        // base class implementation
        // ....
        // then call the child render
        if (childRender) childRender.call(this);
        return this
    },

});

So the child looks completely normal while keeping the base class behaviors.

var ChildView = App.BaseView.extend({
    events: function() {
        return {
            // additional events
        };
    },
    initialize: function(options) {
        // specific initialization
    },
    render: function() {
        // additional rendering
    }
});

Potential problems

This could become a problem if you want to override the base class behavior completely, you would need to cancel some of the base class behavior manually in the child class, and it could prove to be confusing.

Say you have a special child used once that need to override the render completely:

var SpecialChildView = App.BaseView.extend({
    initialize: function(options) {
        // Cancel the base class wrapping by putting 
        // the this class's prototype render back.
        this.render = SpecialChildView.prototype.render;

        // specific initialization
    },
    render: function() {
        // new rendering
    }
});

So it's not black and white and one should evaluate what is needed and what is going to be in the way and choose the right overriding technique.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.