supermodel.js | |
---|---|
(function(Backbone){ | |
The global object. | var root = this; |
Expose Supermodel to the global object. | var Supermodel = root.Supermodel = {}; |
Current version. | Supermodel.VERSION = '0.0.4'; |
Local reference to Collection. | var Collection = Backbone.Collection; |
Use Backbone's | var extend = Backbone.Model.extend; |
AssociationTrack associations between models. Associated attributes are used and
then removed during | var Association = function(model, options) {
this.required(options, 'name');
_.extend(this, _.pick(options, 'name', 'where', 'source', 'store'));
_.defaults(this, {
source: this.name,
store: '_' + this.name
}); |
Store a reference to this association by name after ensuring it's unique. | var ctor = model;
do {
if (!ctor.associations()[this.name]) continue;
throw new Error('Association already exists: ' + this.name);
} while (ctor = ctor.parent);
model.associations()[this.name] = this; |
Listen for relevant events. | if (this.initialize) model.all().on('initialize', this.initialize, this);
if (this.change) model.all().on('change', this.change, this);
if (this.parse) model.all().on('parse', this.parse, this);
if (this.destroy) model.all().on('destroy', this.destroy, this);
if (this.create) model.all().on('add', this.create, this);
};
Association.extend = extend;
_.extend(Association.prototype, { |
Notify | associate: function(model, other) {
if (!this.inverse) return;
model.trigger('associate:' + this.inverse, model, other);
}, |
Notify | dissociate: function(model, other) {
if (!this.inverse) return;
model.trigger('dissociate:' + this.inverse, model, other);
}, |
Throw if the specified options are not provided. | required: function(options) {
var option;
for (var i = 1; i < arguments.length; i++) {
if (options[option = arguments[i]]) continue;
throw new Error('Option required: ' + option);
}
}, |
Wrap a function in order to capture it's context, prepend it to the arguments and call it with the current context. | andThis: function(func) {
var context = this;
return function() {
return func.apply(context, [this].concat(_.toArray(arguments)));
};
}
}); |
OneOne side of a one-to-one or one-to-many association. | var One = Association.extend({
constructor: function(model, options) {
this.required(options, 'inverse', 'model');
Association.apply(this, arguments);
_.extend(this, _.pick(options, 'inverse', 'model'));
_.defaults(this, {
id: this.name + '_id'
});
model.all()
.on('associate:' + this.name, this.replace, this)
.on('dissociate:' + this.name, this.remove, this);
}, |
Assign the getter/setter when a model is created. | create: function(model) {
model[this.name] = _.bind(this.access, this, model);
}, |
Return or replace the associated model. | access: function(model, other) {
if (arguments.length < 2) return model[this.store];
this.replace(model, other);
}, |
Parse the models attributes. If | initialize: function(model) {
this.parse(model, model.attributes);
var id = model.get(this.id);
if (id != null) this.replace(model, id);
}, |
If | parse: function(model, resp) {
if (!_.has(resp, this.source)) return;
var attrs = resp[this.source];
delete resp[this.source];
this.replace(model, attrs);
}, |
Update the association when the | change: function(model) {
if (!model.hasChanged(this.id)) return;
this.replace(model, model.get(this.id));
}, |
Remove the current association. | remove: function(model) {
this.replace(model, null);
}, |
When a model is destroyed, its association should be removed. | destroy: function(model) {
var other = model[this.store];
if (!other) return;
this.remove(model);
this.dissociate(other, model);
}, |
Replace the current association with | replace: function(model, other) {
var id, current;
if (!model) return;
current = model[this.store]; |
If | if (other != null && !_.isObject(other)) {
id = other;
(other = {})[this.model.prototype.idAttribute] = id;
} |
Is | if (other && !(other instanceof Model)) other = this.model.create(other);
if (current === other) return; |
Tear down the current association. | if (!other) model.unset(this.id);
if (current) {
delete model[this.store];
this.dissociate(current, model);
}
if (!other) return; |
Set up the new association. | model.set(this.id, other.id);
model[this.store] = other;
this.associate(other, model);
}
}); |
ManyToOneThe many side of a one-to-many association. | var ManyToOne = Association.extend({
constructor: function(model, options) {
this.required(options, 'inverse', 'collection');
Association.apply(this, arguments);
_.extend(this, _.pick(options, 'collection', 'inverse'));
model.all()
.on('associate:' + this.name, this._associate, this)
.on('dissociate:' + this.name, this._dissociate, this);
}, |
When a model is created, instantiate the associated collection and
assign it using | create: function(model) {
if (!model[this.name]) model[this.name] = _.bind(this.get, this, model);
}, |
Return the associated collection. | get: function(model) {
var collection = model[this.store];
if (collection) return collection; |
Create the collection for storing the associated models. Listen for "add", "remove", and "reset" events and act accordingly. | collection = model[this.store] = new this.collection()
.on('add', this.add, this)
.on('remove', this.remove, this)
.on('reset', this.reset, this); |
We'll need to know what model "owns" this collection in order to handle events that it triggers. | collection.owner = model;
return collection;
}, |
Use the | parse: function(model, resp) {
var attrs = resp[this.source];
if (!attrs) return;
delete resp[this.source];
var collection = this.get(model);
attrs = collection.parse(attrs); |
If | if (!this.where) {
collection.reset(attrs);
return;
} |
Reset the collection after filtering the models from | collection.reset(_.filter(_.map(attrs, function(attrs) {
return new collection.model(attrs);
}), this.where));
}, |
Parse the attributes to initialize a new model. | initialize: function(model) {
this.parse(model, model.attributes);
}, |
Models added to the collection should be associated with the owner. | add: function(model, collection) {
if (!model || !collection) return;
this.associate(model, collection.owner);
}, |
Models removed from the collection should be dissociated from the owner. | remove: function(model, collection) {
if (!model || !collection) return;
this.dissociate(model, collection.owner);
}, |
After a reset, all new models should be associated with the owner. | reset: function(collection) {
if (!collection) return;
collection.each(function(model) {
this.associate(model, collection.owner);
}, this);
}, |
If the owner is destroyed, all models in the collection should be dissociated from it. | destroy: function(model) {
var collection;
if (!model || !(collection = model[this.store])) return;
collection.each(function(other) {
this.dissociate(other, model);
}, this);
}, |
Associated models should be added to the collection. | _associate: function(model, other) {
if (!model || !other) return;
if (this.where && !this.where(other)) return;
this.get(model).add(other);
}, |
Dissociated models should be removed from the collection. | _dissociate: function(model, other) {
if (!model || !other || !model[this.store]) return;
model[this.store].remove(other);
}
}); |
ManyToManyOne side of a many-to-many association. | var ManyToMany = Association.extend({
constructor: function(model, options) {
this.required(options, 'collection', 'through', 'source');
Association.apply(this, arguments);
_.extend(this, _.pick(options, 'collection', 'through'));
this._associate = this.andThis(this._associate);
this._dissociate = this.andThis(this._dissociate);
}, |
When a new model is created, assign the getter. | create: function(model) {
if (!model[this.name]) model[this.name] = _.bind(this.get, this, model);
}, |
Lazy load the associated collection to avoid initialization costs. | get: function(model) {
var collection = model[this.store];
if (collection) return collection; |
Create a new collection. | collection = new this.collection(); |
We'll need to know what model "owns" this collection in order to handle events that it triggers. | collection.owner = model;
model[this.store] = collection; |
Initialize listeners and models. | this.reset(model[this.through]()
.on('add', this.add, this)
.on('remove', this.remove, this)
.on('reset', this.reset, this)
.on('associate:' + this.source, this._associate)
.on('dissociate:' + this.source, this._dissociate));
return collection;
}, |
Add models to the collection when added to the through collection. | add: function(model, through) {
if (!model || !through || !(model = model[this.source]())) return;
if (this.where && !this.where(model)) return;
through.owner[this.name]().add(model);
}, |
Remove models from the collection when removed from the through collection after checking for other instances. | remove: function(model, through) {
if (!model || !through || !(model = model[this.source]())) return;
var exists = through.any(function(o) {
return o[this.source]() === model;
}, this);
if (!exists) through.owner[this.name]().remove(model);
}, |
Reset when the through collection is reset. | reset: function(through) {
if (!through) return;
var models = _.compact(_.uniq(_.invoke(through.models, this.source)));
if (this.where) models = _.filter(models, this.where);
through.owner[this.name]().reset(models);
}, |
Add associated models. | _associate: function(through, model, other) {
if (!through || !model || !other) return;
if (this.where && !this.where(other)) return;
through.owner[this.name]().add(other);
}, |
Remove dissociated models, taking care to check for other instances. | _dissociate: function(through, model, other) {
if (!through || !model || !other) return;
var exists = through.any(function(o) {
return o[this.source]() === other;
}, this);
if (!exists) through.owner[this.name]().remove(other);
}
}); |
hasAvoid naming collisions by providing one entry point for associations. | var Has = function(model) {
this.model = model;
};
_.extend(Has.prototype, { |
oneCreate a one-to-one or one-to-many association. Options:
| one: function(name, options) {
options.name = name;
new One(this.model, options);
return this;
}, |
manyCreate a many-to-one or many-to-many association. Options:
| many: function(name, options) {
options.name = name;
var Association = options.through ? ManyToMany : ManyToOne;
new Association(this.model, options);
return this;
}
}); |
Model | var Model = Supermodel.Model = Backbone.Model.extend({ |
The attribute to store the cid in for lookup. | cidAttribute: 'cid',
initialize: function() { |
Use | this.set(this.cidAttribute, this.cid); |
Add the model to | var ctor = this.constructor;
do { ctor.all().add(this); } while (ctor = ctor.parent); |
Trigger 'initialize' for listening associations. | this.trigger('initialize', this);
}, |
While | toJSON: function() {
var o = Backbone.Model.prototype.toJSON.apply(this, arguments);
delete o[this.cidAttribute];
return o;
}, |
Associations are initialized/updated during | parse: function(resp) {
this.trigger('parse', this, resp);
return resp;
}
}, { |
createCreate a new model after checking for existence of a model with the same id. | create: function(attrs, options) {
var model;
var all = this.all();
var cid = attrs && attrs[this.prototype.cidAttribute];
var id = attrs && attrs[this.prototype.idAttribute]; |
If | if (cid && (model = all.getByCid(cid)) && model.attributes === attrs) {
return model;
} |
If a model already exists for | if (id && (model = all.get(id))) {
model.parse(attrs);
model.set(attrs);
return model;
}
if (!id) return new this(attrs, options); |
Throw if a model already exists with the same id in a superclass. | var ctor = this;
do {
if (!ctor.all().get(id)) continue;
throw new Error('Model with id "' + id + '" already exists.');
} while (ctor = ctor.parent);
return new this(attrs, options);
}, |
Create associations for a model. | has: function() {
return new Has(this);
}, |
Return a collection of all models for a particular constructor. | all: function() {
return this._all || (this._all = new Collection());
}, |
Return a hash of all associations for a particular constructor. | associations: function() {
return this._associations || (this._associations = {});
}, |
Models and associations are tracked via | reset: function() {
this._all = new Collection();
this._associations = {};
}
});
}).call(this, Backbone);
|