How to set Backbone model's defaults based on

2019-05-21 13:13发布

问题:

I need all new MenuItem models to have menu attribute from parent collection. Here is a basic example that doesn't work (because this.collection is undefined in MenuItem's defaults function)

var MenuItem, Menu, menu;

MenuItem = Backbone.Model.extend({
  defaults: function() {
    return {
      menu: this.collection.name
    }
  },

  // Fake Backbone sync
  sync: function(method, model, options) {
    if(typeof model.cid != 'undefined') {
      var cid = model.cid;
      model.unset('cid').set({id:cid}, {silent:true});
    }
    options.success(model);
  }

});

Menu = Backbone.Collection.extend({
  model: MenuItem,
  initialize: function(options) {
    this.name = options.name;
  }
});

menu = new Menu({name: "footer"});

menu.create({title: "Page", url: "/page"}, {
  success: function(model){
    console.log(model.get("menu")) // expect to be "footer"
  }
})

回答1:

I've managed to fix it by overriding collection's create method, I'm still unsure if this is the right way to go.

create: function(attributes, options) {
  return Backbone.Collection.prototype.create.call(
    this,
    _.extend({menu: this.name}, attributes),
    options
  );
}


回答2:

For every possibility where the new model is:

  • a bare object, or a Model instance,
  • added via .add, .push, .create, .set, .reset, or the collection's constructor.

I found that hooking in the _prepareModel undocumented collection function works well.

A generic collection

This collection can be used as-is to replace the default Backbone collection. It adds

  • a new onNewModel fonction to override, that receives the new model instance and the options
  • a custom new-model event which sends the same data.
var Collection = Backbone.Collection.extend({
    /**
     * Hook into the native _prepareModel to offer a standard hook
     * when new models are added to the collection.
     */
    _prepareModel: function(model, options) {
        model = Collection.__super__._prepareModel.apply(this, arguments);
        if (model) {
            // call our new custom callback
            this.onNewModel(model, options);
            // trigger a new custom event
            this.trigger('new-model', model, options);
        }
        return model;
    },

    // Called when adding a new model to the collection.
    onNewModel: _.noop,
});

And your own collection could be:

var Menu = Collection.extend({
    model: MenuItem,
    initialize: function(models, options) {
        this.name = options.name;
    }
    onNewModel: function(model, options) {
        model.set({ menu: model.get('menu') || this.name });
    },
});

It is guarantee that model is a Backbone Model instance inside onNewModel.

Proof of concept

// The generic collection, put that in a file and include it once in your project.
var Collection = Backbone.Collection.extend({
  /**
   * Hook into the native _prepareModel to offer a standard hook
   * when new models are added to the collection.
   */
  _prepareModel: function(model, options) {
    model = Collection.__super__._prepareModel.apply(this, arguments);
    if (model) {
      this.onNewModel(model, options);
      this.trigger('new-model', model, options);
    }
    return model;
  },

  // Called when adding a new model to the collection.
  onNewModel: _.noop,
});

// Extend from the generic collection to make your own.
var Menu = Collection.extend({
  initialize: function(models, options) {
    this.name = options.name;
  },
  onNewModel: function(model, options) {
    model.set({
      menu: model.get('menu') || this.name
    });
    
    console.log("onNewModel menu:", model.get('menu'));
  },
});

// then use it
var menu = new Menu([
  // works with bare objects
  {
    title: "Page",
    url: "/page"
  },
  // or Model instances
  new Backbone.Model({
    title: "Other Page"
  })
], {
  name: "footer" // the collection option
});

// Listen to the custom event if you want
Backbone.listenTo(menu, 'new-model', function(model, options) {
    console.log("'new-model' triggered with", model.get('title'));
});

// or other collection methods
menu.add({
  title: "Page",
  menu: "won't be overriden"
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.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>