Skip to content

Instantly share code, notes, and snippets.

@jasoncrawford
Last active September 5, 2016 16:25
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasoncrawford/7060426 to your computer and use it in GitHub Desktop.
Save jasoncrawford/7060426 to your computer and use it in GitHub Desktop.
Simple relations for Backbone models. Not as full-featured as Backbone-relational (http://backbonerelational.org/) or Supermodel (http://pathable.github.io/supermodel/), but pretty lightweight and concise. Disclaimer: I excerpted this from some other code I wrote and haven't tested it independently. Use at your own risk
var relationEvents = ['add', 'change', 'remove', 'reset', 'sort', 'destroy', 'request', 'sync'];
var Model = exports.Model = Backbone.Model.extend({
hasMany: {
// Subclasses can override to set relations like this:
//
// key: function () { return new Collection([], options); },
},
hasOne: {
// Subclasses can override to set relations like this:
//
// key: function (attrs) { return new Model(attrs, options); },
//
// Note that we don't really care about the difference between hasOne and belongsTo. If you want
// a single model as the value of this key, put it here; if you want a collection, put it under
// hasMany.
},
allRelationKeys: function () {
return Object.keys(this.hasMany).concat(Object.keys(this.hasOne));
},
// Updates the given hasMany relation with the given models, creating a collection for it if
// needed.
updateHasManyRelation: function (key, models, options) {
var collection = this.get(key);
if (!collection) {
var constructor = _.bind(this.hasMany[key], this);
collection = this.attributes[key] = constructor();
collection.parent = this;
this.listenToRelation(key, collection);
}
if (models instanceof Collection) models = models.models;
collection.reset(models, _.extend({silent: false}, options));
},
// Updates the given hasOne relation with the given attributes. If the current value of the key is
// a model with the same ID as the incoming model, updates the current model in-place with the new
// attributes. Otherwise, creates a new model.
updateHasOneRelation: function (key, attributes, options) {
var model = this.get(key);
if (attributes instanceof Model) attributes = attributes.attributes;
if (model && model.id === attributes[this.idAttribute]) {
model.set(attributes);
} else {
var constructor = _.bind(this.hasOne[key], this);
model = constructor(attributes);
this.listenToRelation(key, model);
}
return model;
},
// Listens to events on a relation and proxy them through. E.g., if you have a collection of
// records, and it fires an 'add' event, refire that as 'records:add'.
listenToRelation: function (key, relation) {
var self = this;
var callback = this.onRelationEvent;
relationEvents.forEach(function (event) {
relation.on(event, callback, {parent: self, key: key, event: event});
})
},
// Callback triggered on a relation event, used by listenToRelation
onRelationEvent: function () {
var event = this.key + ':' + this.event;
var args = _.toArray(arguments);
args.unshift(event);
this.parent.trigger.apply(this.parent, args);
},
// Overrides the Backbone.Model#set method to deal with relations, using the update*Relation
// methods.
set: function (key, val, options) {
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (typeof key === 'object') {
attrs = _.clone(key);
options = val;
} else {
(attrs = {})[key] = val;
}
var self = this;
Object.keys(this.hasMany).forEach(function (key) {
if (key in attrs) {
self.updateHasManyRelation(key, attrs[key], options);
delete attrs[key];
}
});
Object.keys(this.hasOne).forEach(function (key) {
if (key in attrs) {
attrs[key] = self.updateHasOneRelation(key, attrs[key], options);
}
});
return Backbone.Model.prototype.set.call(this, attrs, options);
},
// Overrides Backbone.Model#toJSON to deal with relations. Calls toJSON recursively on the
// child collections and adds them to the resulting hash.
toJSON: function () {
var attributes = _.clone(this.attributes);
this.allRelationKeys().forEach(function (key) {
if (key in attributes) {
attributes[key] = attributes[key].toJSON();
}
})
return attributes;
}
})
var User = Model.extend({
urlRoot: function () {
// Let the collection, if any, define the create URL for new models
if (this.isNew() && this.collection && this.collection.url) return null;
return '/users';
}
})
var Users = Backbone.Collection.extend({
model: User
})
var Group = Model.extend({
hasMany: {
users: function () { return new Users([], {url: function () { return this.parent.url() + '/users'; }}); },
},
hasOne: {
owner: function (attrs) { return new User(attrs); },
},
})
// Now you can do:
var group = new Group({id: '1234'});
group.get('users'); // --> undefined, not yet populated
group.get('owner'); // --> ditto
group.fetch(); // assume this returns JSON including { owner: {id: 1, name: ...}, users: [{id: 1, name: ...}, {id: 2, name: ...}]}
group.get('users'); // --> a collection of User models
group.get('owner'); // --> a User model
var user = group.get('users').create({name: 'John'}); // does a POST to /groups/1234/users to create the user in the group
user.save({...}); // now that the user is created, this will PUT directly to /users/:id
// To get notified when a user is added to the group:
group.listenTo('users:add', function (user, collection) { ... });
// To get notified when the owner attributes change:
group.listenTo('owner:change', function (user), { ... });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment