Skip to content

Instantly share code, notes, and snippets.

@gaearon
Last active December 23, 2015 20:29
Show Gist options
  • Save gaearon/6689379 to your computer and use it in GitHub Desktop.
Save gaearon/6689379 to your computer and use it in GitHub Desktop.
A base Backbone model that supports nested models/collections with simple syntax. See http://stackoverflow.com/a/18989596/458193
// First, define `nestedTypes` in your model.
// Each nested type may be a Model or Collection subtype.
window.app.viewer.Model.GallerySection = window.app.Model.BaseModel.extend({
nestedTypes: {
background: window.app.viewer.Model.Image,
images: window.app.viewer.Collection.MediaCollection
}
});
// That's it!
// Now you can call constructor with JSON, fetch the model from server, or pass JSON to `set` on individual properties.
// Overriden `set` will correctly instantiate corresponding model or collection if you pass JSON to it.
// Overriden `toJSON` will call `toJSON` on nested types.
var gallery = new window.app.viewer.Model.GallerySection({
background: { url: 'http://example.com/example.jpg' },
images: [
{ url: 'http://example.com/1.jpg' },
{ url: 'http://example.com/2.jpg' },
{ url: 'http://example.com/3.jpg' }
],
title: 'Wow'
});
// Nested models and collections have been created automatically
console.log(gallery.get('background')); // window.app.viewer.Model.Image
console.log(gallery.get('images')); // window.app.viewer.Collection.MediaCollection
// And you can call `set` with JSON because it knows the type to instantiate.
gallery.set('background', { url: 'http://example.com/example.jpg' });
// ----------------
window.app.Model.BaseModel = Backbone.Model.extend({
constructor: function () {
if (this.nestedTypes) {
this.checkNestedTypes();
}
Backbone.Model.apply(this, arguments);
},
set: function (key, val, options) {
var attrs;
/* jshint -W116 */
/* jshint -W030 */
// Code below taken from Backbone 1.0 to allow different parameter styles
if (key == null) return this;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// Code above taken from Backbone 1.0 to allow different parameter styles
/* jshint +W116 */
/* jshint +W030 */
// What we're trying to do here is to instantiate Backbone models and collections
// with types defined in this.nestedTypes, and use them instead of plain objects in attrs.
if (this.nestedTypes) {
attrs = this.mapAttributes(attrs, this.deserializeAttribute);
}
return Backbone.Model.prototype.set.call(this, attrs, options);
},
toJSON: function () {
var json = Backbone.Model.prototype.toJSON.apply(this, arguments);
if (this.nestedTypes) {
json = this.mapAttributes(json, this.serializeAttribute);
}
return json;
},
mapAttributes: function (attrs, transform) {
transform = _.bind(transform, this);
var result = {};
_.each(attrs, function (val, key) {
result[key] = transform(val, key);
}, this);
return result;
},
serializeAttribute: function (val, key) {
var NestedType = this.nestedTypes[key];
if (!NestedType) {
return val;
}
if (_.isNull(val) || _.isUndefined(val)) {
return val;
}
return val.toJSON();
},
deserializeAttribute: function (val, key) {
var NestedType = this.nestedTypes[key];
if (!NestedType) {
return val;
}
var isCollection = this.isTypeASubtypeOf(NestedType, Backbone.Collection),
child;
if (val instanceof Backbone.Model || val instanceof Backbone.Collection) {
child = val;
} else if (!isCollection && (_.isNull(val) || _.isUndefined(val))) {
child = null;
} else {
child = new NestedType(val);
}
var prevChild = this.get(key);
// Return existing model if it is equal to child's attributes
if (!isCollection && child && prevChild && _.isEqual(prevChild.attributes, child.attributes)) {
return prevChild;
}
return child;
},
isTypeASubtypeOf: function (DerivedType, BaseType) {
// Go up the tree, using Backbone's __super__.
// This is not exactly encouraged by the docs, but I found no other way.
if (_.isUndefined(DerivedType['__super__'])) {
return false;
}
var ParentType = DerivedType['__super__'].constructor;
if (ParentType === BaseType) {
return true;
}
return this.isTypeASubtypeOf(ParentType, BaseType);
},
checkNestedTypes: function () {
_.each(this.nestedTypes, function (val, key) {
if (!_.isFunction(val)) {
console.log('Not a function:', val);
throw new Error('Invalid nestedTypes declaration for key ' + key + ': expected a function');
}
});
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment