Phillip Whisenhunt
Phillip Whisenhunt

Reputation: 1305

Default values of Backbone.js view?

I'm working in Backbone.js and I was wondering if you can set default values much the same way that you can set the default values of a model?

Upvotes: 19

Views: 18066

Answers (10)

Brian Genisio
Brian Genisio

Reputation: 48137

For Backbone 1.1 or newer

Approach A: OptionsInLiteral with _.defaults in initialize

var MyView = Backbone.View.extend({
  options: {
    enabled: true,
    align:   "left"
  },
  initialize: function (options) {
    #be sure to do the '|| {}' here so 'new MyView()' works
    this.options = _.defaults(options || {}, this.options);
  }
});

Approach B: Use the viewOptions plugin (or similar)

backbone.viewOptions

Thanks to @BraveDave for pointing this out in a comment.

For Backbone prior to version 1.1 (historical reference, FYI)

Here is the backbone issue where it seems like the core team is most likely to get rid of this.options and the logic in _configure altogether.

Use an options property and always use this.options

There is much confusion on this question and even a highly upvoted and accepted incorrect answer. Hopefully this answer demonstrates a truly correct solution as well as pointing out the bugs in all the other candidate answers.

To work in harmony with the Backbone.View parent class, you are supposed to include an options property of the object literal you pass to Backbone.View.extend.

var OptionsInLiteral = Backbone.View.extend({
  options: {flavor: "vanilla"},
  initialize: function (options) {
    console.log("OptionsInLiteral.initialize first argument", options);
    console.log("OptionsInLiteral.initialize this.options", this.options);
  }
});

Here are some examples and what they log to the console.

new OptionsInLiteral();
    //OptionsInLiteral.initialize first argument undefined
    //OptionsInLiteral.initialize this.options Object {flavor: "vanilla"}
new OptionsInLiteral({flavor: "chocolate"});
    //OptionsInLiteral.initialize first argument Object {flavor: "chocolate"}
    //OptionsInLiteral.initialize this.options Object {flavor: "chocolate"}
new OptionsInLiteral({flavor: "strawberry", sprinkles: true});
    //OptionsInLiteral.initialize first argument Object {flavor: "strawberry", sprinkles: true}
    //OptionsInLiteral.initialize this.options Object {flavor: "strawberry", sprinkles: true}

This will correctly take advantage of Backbone.View._configure, which as of Backbone 1.0.0 looks like this:

_configure: function(options) {
  if (this.options) options = _.extend({}, _.result(this, 'options'), options);
  _.extend(this, _.pick(options, viewOptions));
  this.options = options;
},

What this means is:

  • If your view object literal contains options, _configure will properly treat those as default values, override them with properties passed into the constructor, and set the final resulting object as this.options. Hurray. That's what we want.
  • This will work even if the view constructor is invoked without arguments. Hurray. Also what we want.
  • Because _.result is used here, the options property may be either an Object or a function, and if it's a function, it will be called and the return value will be used.

This is also acceptable and allows the defaults to be unique per instance.

var OptionsFunctionInLiteral = Backbone.View.extend({
  options: function () {
    return {
      flavor: "vanilla",
      created: Date(),
      collection: new Backbone.Collection()
    };
  },
  initialize: function (options) {
    console.log("OptionsFunctionInLiteral.initialize first argument", options);
    console.log("OptionsFunctionInLiteral.initialize this.options", this.options);
  }
});

Here are some examples and what they log to the console.

new OptionsFunctionInLiteral();
    //OptionsFunctionInLiteral.initialize first argument undefined
    //OptionsFunctionInLiteral.initialize this.options Object {flavor: "vanilla", created: "Wed Jun 19 2013 16:20:16 GMT-0600 (MDT)", collection: Backbone.Collection}
new OptionsFunctionInLiteral({flavor: "chocolate"});
    //OptionsFunctionInLiteral.initialize first argument Object {flavor: "chocolate"}
    //OptionsFunctionInLiteral.initialize this.options Object {flavor: "chocolate", created: "Wed Jun 19 2013 16:21:17 GMT-0600 (MDT)", collection: Backbone.Collection}
new OptionsFunctionInLiteral({flavor: "strawberry", sprinkles: true});
    //OptionsFunctionInLiteral.initialize first argument Object {flavor: "strawberry", sprinkles: true}
    //OptionsFunctionInLiteral.initialize this.options Object {flavor: "strawberry", created: "Wed Jun 19 2013 16:22:26 GMT-0600 (MDT)", collection: Backbone.Collection, sprinkles: true}

Why you should always use this.options

So the above is great with the caveat that if your view's constructor is called with no arguments, inside your initialize function this.options will exist and be correct but the plain options argument to the initialize function will be undefined.

initialize: function (options) {
  console.log(options.flavor); //BUG! options is undefined. Uncaught exeption. :-(
  console.log(this.options); //correct
}

Thus when I define my initialize, I don't even specify the options argument to the function as a reminder not to use it. In general you want to ignore the options argument to initialize because it doesn't contain the default values anyway.

Buggy answer: _.extend(this.defaults, this.options)

This answer has a bug in that it unintentionally modifies the defaults for all future instances every time an instance is instatiated.

var DefaultsExtendView = Backbone.View.extend({
  defaults: {flavor: "vanilla"},
  initialize: function (options) {
    console.log("initialize 1st argument", options);
    this.options = _.extend(this.defaults, this.options);
    console.log("initialize this.options", this.options);
  }
});

new DefaultsExtendView(); //OK
new DefaultsExtendView({flavor: "chocolate"}); //OK
new DefaultsExtendView(); //BUG! You get chocolate instead of vanilla

Buggy answer: if (options.foo)

var myView = Backbone.View.extend({
    foo: "default_value",

    initialize: function(options) {
        if(options.foo) {
            foo = options.foo;
        }
    }
});

new myView(); //BUG! options is undefined, uncaught exception
//TypeError: Cannot read property 'foo' of undefined

Beware of options object and instance-specific defaults

One of the answers to this question suggests this:

var DefaultsView = Backbone.View.extend({
  defaults: {
    collection: new Backbone.Collection()
  },
  initialize: function () {
    _.defaults(this.options, this.defaults);

Which is almost certainly not what you want and a bug. If you make 10 views, they will all be sharing the same instance of Backbone.Collection as there will just be 1 instance created when the view subclass is defined. This is sure to confuse you when you add a model to view9's collection and it shows up in all of the views. What you more likely want is a different new collection instance for each view instance, and to get that you need to make options be a function as in my example above.

Summary of the proper way to do this

  1. use options: {...} or options: function () {...}
  2. Declare your initialize without any arguments
  3. Access your properly-defaulted options as this.options

Example Boilerplate

var MyView = Backbone.View.extend({
  options: {flavor: "vanilla"},
  initialize: function () { //note no declared arguments
      //use this.options here as needed and all is well
  }
});

Working jsfiddle

http://jsfiddle.net/DUc25/

Upvotes: 12

Peter Lyons
Peter Lyons

Reputation: 146064

For Backbone 1.1 or newer

Approach A: OptionsInLiteral with _.defaults in initialize

var MyView = Backbone.View.extend({
  options: {
    enabled: true,
    align:   "left"
  },
  initialize: function (options) {
    #be sure to do the '|| {}' here so 'new MyView()' works
    this.options = _.defaults(options || {}, this.options);
  }
});

Approach B: Use the viewOptions plugin (or similar)

https://github.com/rotundasoftware/backbone.viewOptions

Thanks to @BraveDave for pointing this out in a comment.

For Backbone prior to version 1.1 (historical reference, FYI)

Here is the backbone issue where it seems like the core team is most likely to get rid of this.options and the logic in _configure altogether.

Use an options property and always use this.options

There is much confusion on this question and even a highly upvoted and accepted incorrect answer. Hopefully this answer demonstrates a truly correct solution as well as pointing out the bugs in all the other candidate answers.

To work in harmony with the Backbone.View parent class, you are supposed to include an options property of the object literal you pass to Backbone.View.extend.

var OptionsInLiteral = Backbone.View.extend({
  options: {flavor: "vanilla"},
  initialize: function (options) {
    console.log("OptionsInLiteral.initialize first argument", options);
    console.log("OptionsInLiteral.initialize this.options", this.options);
  }
});

Here are some examples and what they log to the console.

new OptionsInLiteral();
    //OptionsInLiteral.initialize first argument undefined
    //OptionsInLiteral.initialize this.options Object {flavor: "vanilla"}
new OptionsInLiteral({flavor: "chocolate"});
    //OptionsInLiteral.initialize first argument Object {flavor: "chocolate"}
    //OptionsInLiteral.initialize this.options Object {flavor: "chocolate"}
new OptionsInLiteral({flavor: "strawberry", sprinkles: true});
    //OptionsInLiteral.initialize first argument Object {flavor: "strawberry", sprinkles: true}
    //OptionsInLiteral.initialize this.options Object {flavor: "strawberry", sprinkles: true}

This will correctly take advantage of Backbone.View._configure, which as of Backbone 1.0.0 looks like this:

_configure: function(options) {
  if (this.options) options = _.extend({}, _.result(this, 'options'), options);
  _.extend(this, _.pick(options, viewOptions));
  this.options = options;
},

What this means is:

  • If your view object literal contains options, _configure will properly treat those as default values, override them with properties passed into the constructor, and set the final resulting object as this.options. Hurray. That's what we want.
  • This will work even if the view constructor is invoked without arguments. Hurray. Also what we want.
  • Because _.result is used here, the options property may be either an Object or a function, and if it's a function, it will be called and the return value will be used.

This is also acceptable and allows the defaults to be unique per instance.

var OptionsFunctionInLiteral = Backbone.View.extend({
  options: function () {
    return {
      flavor: "vanilla",
      created: Date(),
      collection: new Backbone.Collection()
    };
  },
  initialize: function (options) {
    console.log("OptionsFunctionInLiteral.initialize first argument", options);
    console.log("OptionsFunctionInLiteral.initialize this.options", this.options);
  }
});

Here are some examples and what they log to the console.

new OptionsFunctionInLiteral();
    //OptionsFunctionInLiteral.initialize first argument undefined
    //OptionsFunctionInLiteral.initialize this.options Object {flavor: "vanilla", created: "Wed Jun 19 2013 16:20:16 GMT-0600 (MDT)", collection: Backbone.Collection}
new OptionsFunctionInLiteral({flavor: "chocolate"});
    //OptionsFunctionInLiteral.initialize first argument Object {flavor: "chocolate"}
    //OptionsFunctionInLiteral.initialize this.options Object {flavor: "chocolate", created: "Wed Jun 19 2013 16:21:17 GMT-0600 (MDT)", collection: Backbone.Collection}
new OptionsFunctionInLiteral({flavor: "strawberry", sprinkles: true});
    //OptionsFunctionInLiteral.initialize first argument Object {flavor: "strawberry", sprinkles: true}
    //OptionsFunctionInLiteral.initialize this.options Object {flavor: "strawberry", created: "Wed Jun 19 2013 16:22:26 GMT-0600 (MDT)", collection: Backbone.Collection, sprinkles: true}

Why you should always use this.options

So the above is great with the caveat that if your view's constructor is called with no arguments, inside your initialize function this.options will exist and be correct but the plain options argument to the initialize function will be undefined.

initialize: function (options) {
  console.log(options.flavor); //BUG! options is undefined. Uncaught exeption. :-(
  console.log(this.options); //correct
}

Thus when I define my initialize, I don't even specify the options argument to the function as a reminder not to use it. In general you want to ignore the options argument to initialize because it doesn't contain the default values anyway.

Buggy answer: _.extend(this.defaults, this.options)

This answer has a bug in that it unintentionally modifies the defaults for all future instances every time an instance is instatiated.

var DefaultsExtendView = Backbone.View.extend({
  defaults: {flavor: "vanilla"},
  initialize: function (options) {
    console.log("initialize 1st argument", options);
    this.options = _.extend(this.defaults, this.options);
    console.log("initialize this.options", this.options);
  }
});

new DefaultsExtendView(); //OK
new DefaultsExtendView({flavor: "chocolate"}); //OK
new DefaultsExtendView(); //BUG! You get chocolate instead of vanilla

Buggy answer: if (options.foo)

var myView = Backbone.View.extend({
    foo: "default_value",

    initialize: function(options) {
        if(options.foo) {
            foo = options.foo;
        }
    }
});

new myView(); //BUG! options is undefined, uncaught exception
//TypeError: Cannot read property 'foo' of undefined

Beware of options object and instance-specific defaults

One of the answers to this question suggests this:

var DefaultsView = Backbone.View.extend({
  defaults: {
    collection: new Backbone.Collection()
  },
  initialize: function () {
    _.defaults(this.options, this.defaults);

Which is almost certainly not what you want and a bug. If you make 10 views, they will all be sharing the same instance of Backbone.Collection as there will just be 1 instance created when the view subclass is defined. This is sure to confuse you when you add a model to view9's collection and it shows up in all of the views. What you more likely want is a different new collection instance for each view instance, and to get that you need to make options be a function as in my example above.

Summary of the proper way to do this

  1. use options: {...} or options: function () {...}
  2. Declare your initialize without any arguments
  3. Access your properly-defaulted options as this.options

Example Boilerplate

var MyView = Backbone.View.extend({
  options: {flavor: "vanilla"},
  initialize: function () { //note no declared arguments
      //use this.options here as needed and all is well
  }
});

Working jsfiddle

http://jsfiddle.net/DUc25/

Upvotes: 25

Brave Dave
Brave Dave

Reputation: 1300

Use the backbone.viewOptions plugin, which does exactly what you are looking for:

// Add the view options plugin functionality to all our views.
Backbone.ViewOptions.add( Backbone.View.prototype );

MyView = Backbone.View.extend( {
    options : [
        "type", // normal options are declared like so
        { "label" : "OK" } // options with defaults are declared like so
     ],

    initialize : function( options ) {
        this.setOptions( options ); // attaches declared options to the view

        console.log( this.label ); // outputs "OK"
        console.log( this.type ); // outputs "button"
    }
} );

new MyView( { type : "button" } );

Upvotes: 0

jseto
jseto

Reputation: 51

If I understand the question correctly, you can set defaults in this way:

scope.MyView = Backbone.View.extend({
    x: 'x',
})

var obj = new scope.MyView({
    y: 'y',
});

console.log( this.options );
// output: { x: 'x', y:'y' }

The problem, and the behavior does not fully reflect what is stated in the View constructor documentation, is that compound objects are not fully copied. This is because _configure uses underscore.js _.extend and it is not recursive.

That means that if you do something like this:

scope.MyView = Backbone.View.extend({
    x: {
        a: 'a',
    }
})

var obj = new scope.MyView({
    x: {
        b: 'b',
    }
});

console.log( this.options );
// output: { x: { b:'b' } }

does not work as expected. This is likely to happen if you pass attributes to the view an have default attributes. The attributes you pass will be overwritten.

Upvotes: 0

Blueshirts
Blueshirts

Reputation: 309

This stack is a little misleading to me. Peter Lyons answer seems like the current correct one though does not have the most votes.

From the Backbone docs...

When creating a new View, the options you pass — after being merged into any default options already present on the view — are attached to the view as this.options for future reference.

http://backbonejs.org/#View-constructor

I was able to make it work by implementing an options in the class definition.

MyScope.MyView = Backbone.View.extend({
    options: {
        default_prop: 'value1',
        another_default: 'value2'
    }
}

Upvotes: 0

dira
dira

Reputation: 4791

What you can do is to set your defaults in the initialize function.

defaults: {
  display: 'list'
},

initialize: function() {
  this.options = _.extend({}, this.defaults, this.options);
}

This will work for normal options, but would not override any special options (the ones that Backbone stores on the view object as well - ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'])

See a working demo: http://jsfiddle.net/dira/7MmQE/1/

Upvotes: 30

Ronni Egeriis Persson
Ronni Egeriis Persson

Reputation: 2289

The correct solution is similar to dira's. Simply specifying an options object in the view spec will contain default values for the options object: http://jsfiddle.net/h3cAU/1/

var ViewDefaultsDemo = Backbone.View.extend({
  options: {
     display: 'list'
  },

  render: function() {
    console.log(this.options.display)
  }
});

See the source for View._configure for additional info: https://github.com/documentcloud/backbone/blob/0.9.10/backbone.js#L1332-L1334

Upvotes: 0

ddan
ddan

Reputation: 23

An attempt at duck punching. From Backbone's source code:

var View = Backbone.View = function(options) {
    this.cid = _.uniqueId('view');
    this._configure(options || {});
    this._ensureElement();
    this.initialize.apply(this, arguments);
    this.delegateEvents();
};

We'll override _configure:

// Save _configure method
var _oldConfigure = Backbone.View.prototype._configure;

Backbone.View.prototype._configure = function(options){
    _.defaults(options, this.defaults); 
    _oldConfigure.call(this, options);
};

Now Backbone.View behaves the same as Backbone.model with regards to defaults, and you don't even need to do anything in the constructor/initialize method.

Upvotes: 2

yun_cn
yun_cn

Reputation: 83

By Backbone.View's explanation, it says

constructor / initializenew View([options])

When creating a new View, the options you pass are attached to the view as this.options, for future reference. There are several special options that, if passed, will be attached directly to the view: model, collection, el, id , className, tagName and attributes. If the view defines an initialize function, it will be called when the view is first created. If you'd like to create a view that references an element already in the DOM, pass in the element as an option: new View({el: existingElement}).

I wounder why the defaults is not used in the view in the same way that are used in the Model and Collection.

Upvotes: 1

Andrew
Andrew

Reputation: 1650

var DefaultsView = Backbone.View.extend({
  defaults: {
    collection: new Backbone.Collection()
  },
  initialize: function () {
    _.defaults(this.options, this.defaults);
    // Ensures keys with special meaning (model, collection, id, className, etc.), are attached directly to the view
    Backbone.View.prototype._configure.apply(this, arguments);
  },
  render: function () {
    console.log(this.collection);
  }
});

var view = new DefaultsView();

view.render();

Upvotes: 2

Related Questions