1

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);
}
Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
user2326737
  • 211
  • 1
  • 4
  • 16

1 Answers1

1

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.

Proof of concept

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.

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129