Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending backbone view and inherit options in a clean manner

This is my first question on SO so I hope I am not messing up. I have checked other topics on this problem, but they do not cover the case that I am having.

I am building a library on top of Backbone to create mobile applications. The main reason I am defining all components as a backbone views is because I want to do memory optimalisation on scrolling (hiding stuff / removing stuff from the DOM).

Let me just start off with a defenition that would be the most ideal situation

Define a base class for other components to use, with some default properties and some handy methods that I need on every component.

UI.Component = Backbone.View.extend({
    viewOptions: ['children'],

    children: [],

    add: function(child) {
        this.children.push(child);
    }
});

Define a component with some default properties

UI.Header = UI.Component.extend({
    viewOptions: ['titleBarChildren', 'secondaryBarChildren', 'title', 'backButtonTitle'],

    titleBarChildren: [],

    secondaryBarChildren: [],

    title: '',

    backButtonTitle: 'Back'
});

Making a default header to use in my application

MyApp.Headers.Default = UI.Header.extend({
    backButtonTitle: 'Terug',

    titleBarChildren: [
        new UI.SegmentedController({
            children: [
                new UI.Button('Lame example')
            ]
        })
    ]
});

Using my navbar

var homePageNavbar = new MyApp.Header.Default({
    title: 'Homepage'
});

Now let's run

console.log(homePageNavbar);

And imagine we get this output

// My properties, all nicely inherited
viewOptions: Array[5] //['children', 'titleBarChildren', 'secondaryBarChildren', 'title', 'backButtonTitle']
children: Array[0],
titleBarChildren: Array[1],
secondaryBarChildren: Array[0],
title: "Homepage",
backButtonTitle: "Terug"

// Some stuff that backbone assigns
$el: jQuery.fn.jQuery.init[1]
cid: "view12"
el: HTMLDivElement
options: Object
__proto__: ctor

That's the result I am trying to get in the end, this however requires some magic that is beyond my experience.

On a side note, I have tried using the "options" property for all my custom stuff, but the problem there is that when I create multiple view instances, the options are "shared" / referencing eachother.

So now I am hoping I will have more luck with the "viewOptions" approach, and then overriding the _configure method

Preferrably this is all done in the UI.Component or it's children. I don't want users of this library to add some "boilerplate code" in their header defenition (MyApp.Headers.Default = UI.Header.extend) to extend options or whatever.

Now I somehow have to get all the properies from all the "decendants" combined. and I have absolutely no clue how to go about it. I have tried following what goes on behind the scenes in backbone but I can't wrap my head around it.

So tips / tricks on getting this done are very much appreciated, also, alternative ways, or ways that don't match my exact requirements are very welcome.

EDIT 1

It looks like I have something that "sort of" works, Here's what it looks like

I override the _configure method, to add the options onto the instance, note the this.getDefaultOptions() as the first argument in _.extend

UI.Component = Backbone.View.extend({
  _configure: function(options) {
    var viewOptions = this.viewOptions;
    options = _.extend(this.getDefaultOptions ? this.getDefaultOptions() : {}, this.options || {}, options || {});
    for (var i = 0, l = viewOptions.length; i < l; i++) {
      var attr = viewOptions[i];
      if (options[attr]) this[attr] = options[attr];
    }
    this.options = options;
  }
});

I added this new method to all my components, and did not put "shared" properties in my base object (UI.Component) since I couln't get that to play nice.

UI.Header = UI.Component.extend({

  viewOptions: ['children', 'backButtonTitle', 'title'],

  getDefaultOptions: function() {
    return {
      children: []
    };
  },
});

Now, I use something like this to define my header

MyApp.Headers.Default = UI.Header.extend({
  options: {
    backButtonTitle: 'Terug',
    titleBarChildren: [
      new UI.SegmentedController({
        children: [
          new UI.Button('Lame example')
        ]
      })
    ]
  }
});

I will keep it the way it is now and see if this "solution" survives, and will report back. If you think you have a better answer, don't hestitate to post it =)

EDIT

This is my latest approach, using jQuery's deep copy, it seems to work allright

var UI = {};
var App = {
  Headers: {}
};

UI.Component = Backbone.View.extend({
  blockDefaults: {
    children: []
  },

  initialize: function(options) {
    var combinedDefaultsAndOptions = $.extend(true, {}, this.blockDefaults || {}, this.defaults || {}, this.options || {}, options || {});
    _.defaults(this, combinedDefaultsAndOptions);
  }
});

UI.Header = UI.Component.extend({
  defaults: {
    backButton: null,
    backButtonTitle: null,
    secondaryBarChildren: []
  }
});

App.Headers.Default = UI.Header.extend({
  options: {
    backButtonTitle: "App back",
    secondaryBarChildren: ["App child"]
  }
});

var header1 = new UI.Header({
  backButtonTitle: "Header 1 Back",
  secondaryBarChildren: ["Header 1 child"]
});

var header2 = new UI.Header({
  backButtonTitle: "Header 2 Back",
  secondaryBarChildren: ["Header 2 child"]
});

var header3 = new App.Headers.Default({
});

var header4 = new App.Headers.Default({
  backButtonTitle: "Overrided App Back"
});

header1.secondaryBarChildren.push('a child for header 1');

console.log(header1, header2, header3, header4);
like image 300
Vespakoen Avatar asked Nov 20 '12 18:11

Vespakoen


1 Answers

I think you want to override the initialize function for your example but you have a lot going on so I'm not sure; forgive me if this is off. And I might just be cleaning up what I think you're trying to do into a more 'Backbone' style system.

Start with your base component like this:

UI.Component = Backbone.View.extend({
    defaults : { 'children' : [] },
    initialize : function (options) {
        _.defaults(options, this.defaults);
        _.keys(this.defaults).forEach(function(key) {
              this[key] = this.options[key];
        }.bind(this));
        Backbone.View.prototype.initialize.call(this, options);
    },
    add: function(child) {
        this.children.push(child);
    }
});

Then you can continue to extend them in a similar way like this:

UI.Header = UI.Component.extend({
    defaults : { 
        'titleBarChildren' : [], 
        'secondaryBarChildren' : [], 
        'title' : '', 
        'backButtonTitle' : 'Back' 
    },
    initialize : function (options) {
        _.defaults(options, this.defaults);
        _.keys(this.defaults).forEach(function(key) {
              this[key] = this.options[key];
        }.bind(this));
        UI.Component.prototype.initialize.call(this, options);
    }
});

Here's the breakdown of what I'm trying to do in the code:

Defaults creates an object that holds all your default attributes cleanly

    defaults : { 'children' : [] },

Initialize override allows you to continue passing options from view to view but each component uses your defaults object to fill in the gaps.

    initialize : function (options) {
        // merges your defaults in with the options passed
        _.defaults(options, this.defaults);
        // loop through your required defaults for this view
        // assigning the attributes to the passed options
        _.keys(this.defaults).forEach(function(key) {
              this[key] = this.options[key];
        }.bind(this));
        // Finally pass all your options up the chain of inheritance
        UI.Component.prototype.initialize.call(this, options);
    },
like image 53
Bryan Clark Avatar answered Nov 06 '22 09:11

Bryan Clark