javascriptbackbone.jsbackbone-eventsbackbone-model

Clear all attributes except one on a Backbone Model


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

Solution

  • 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.