简体   繁体   中英

Default values of Backbone.js view?

我在Backbone.js工作,我想知道你是否可以像设置模型的默认值一样设置默认值?

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/

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/

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/

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();

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.

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.

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

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'
    }
}

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.

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" } );

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM