I have a model that has more attributes than the default attributes. I need to clear all attributes when guest
changes and set back to defaults so I don't carry unnecessary attributes.
Clearing all attributes and setting the defaults back causes an infinite loop because of the change:guest
event.
How can I delete all of the attributes except one?
Is there a way not to fire another change event when Model attributes are set back to the defaults?
Or delete anything not listed in the defaults?
Here is my Model
defaults: {
_id: 'id',
first_name: 'first_name',
last_name: 'last_name',
guest: true
}
I listen to 'guest' change event
this.on('change:guest', this.reset);
The change event calls reset
to update the Model and obviously this causes an infinite loop.
reset: function() {
var new_defaults = _.clone(this.defaults);
this.clear({silent: true});
this.set(new_defaults);
}
I have made a reset
function that you can easily add to a base Backbone model. I go into more details about this solution into another answer.
It's better than a simple .clear
followed by a .set
because it merges the defaults
back into the model, letting any passed attributes
to override them like on initialization.
/**
* Clears the model's attributes and sets the default attributes.
* @param {Object} attributes to overwrite defaults
* @param {Object} options to pass with the "set" call.
* @return {Backbone.Model} this object, to chain function calls.
*/
reset: function(attributes, options) {
options = _.extend({ reset: true }, options);
// ensure default params
var defaults = _.result(this, 'defaults'),
attrs = _.defaults(_.extend({}, defaults, attributes || {}), defaults);
// apply
this._reset(attrs, options);
// triggers a custom event, namespaced to model in order
// to avoid collision with collection's native reset event
// when listening to a collection.
if (!options.silent) this.trigger('model:reset', this, options);
return this;
},
/**
* Private method to help wrap reset with a custom behavior in child
* classes.
* @param {Object} attributes to overwrite defaults
* @param {Object} options to pass with the "set" call.
*/
_reset: function(attrs, options) {
this.clear({ silent: true }).set(attrs, options);
},
Then your model:
var MyModel = BaseModel.extend({
idAttribute: '_id',
defaults: {
first_name: 'first_name',
last_name: 'last_name',
guest: true
},
initialize: function() {
this.listenTo(this, 'change:guest', this.onGuestChange);
},
onGuestChange: function(model, value, options) {
this.reset(null, { silent: true });
}
});
This way, you have more flexibility on what happens when guest
changes with the onGuestChange
handler, which makes it possible to call reset
however you like, here with { silent: true }
option.
var BaseModel = Backbone.Model.extend({
/**
* Clears the model's attributes and sets the default attributes.
* @param {Object} attributes to overwrite defaults
* @param {Object} options to pass with the "set" call.
* @return {Backbone.Model} this object, to chain function calls.
*/
reset: function(attributes, options) {
options = _.extend({
reset: true
}, options);
// ensure default params
var defaults = _.result(this, 'defaults'),
attrs = _.defaults(_.extend({}, defaults, attributes || {}), defaults);
// apply
this._reset(attrs, options);
// triggers a custom event, namespaced to model in order
// to avoid collision with collection's native reset event
// when listening to a collection.
if (!options.silent) this.trigger('model:reset', this, options);
return this;
},
/**
* Private method to help wrap reset with a custom behavior in child
* classes.
* @param {Object} attributes to overwrite defaults
* @param {Object} options to pass with the "set" call.
*/
_reset: function(attrs, options) {
this.clear({
silent: true
}).set(attrs, options);
},
})
var MyModel = BaseModel.extend({
defaults: {
first_name: 'first_name',
last_name: 'last_name',
guest: true
},
initialize: function() {
this.listenTo(this, 'change:guest', this.onGuestChange);
},
onGuestChange: function(model, value, options) {
this.reset(null, {
silent: true
});
}
});
var model = new MyModel({
first_name: 'test',
});
console.log('before:', model.attributes);
model.set('guest', false);
console.log('after:', model.attributes);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
You don't need to clone the defaults to use them. If they have an array or nested objects, defaults
should be a function returning an object.
defaults: function() {
return {
arr: [],
nested: { prop: 'test' }
};
},
Then, use _.result
to call the defaults: _.result(this, 'defaults')
like in my reset function. The Backbone documentation on defaults
has this notice:
Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances. Instead, define defaults as a function.