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