Backbone-associations
Backbone-associations provides a way of specifying 1:1 and 1:N relationships between Backbone models. Additionally, parent model instances (and objects extended from Backbone.Events
) can listen in to CRUD events initiated on any children - in the object graph - by providing an appropriately qualified event path name. It aims to provide a clean implementation which is easy to understand and extend. It is performant for CRUD operations - even on deeply nested object graphs - and uses a low memory footprint. Web applications leveraging the client-side-MVC architectural style will benefit by using backbone-associations
to define and manipulate client object graphs.
It comes with
- The annotated source code.
- An online test suite which includes backbone test cases run with
AssociatedModel
s. - Performance tests.
It was originally born out of a need to provide a simpler and speedier implementation of Backbone-relational
Contents
Backbone-associations depends on backbone (and thus on underscore). Include Backbone-associations right after Backbone and Underscore:
<script type="text/javascript" src="./js/underscore.js"></script>
<script type="text/javascript" src="./js/backbone.js"></script>
<script type="text/javascript" src="./js/backbone-associations.js"></script>
Backbone-associations works with Backbone v0.9.10. Underscore v1.4.3 upwards is supported.
Each Backbone.AssociatedModel
can contain an array of relations
. Each relation defines a relatedModel
, key
, type
and (optionally) collectionType
. This can be easily understood by some examples.
Specifying One-to-One Relationship
var Employee = Backbone.AssociatedModel.extend({
relations: [
{
type: Backbone.One,
key: 'manager',
relatedModel: 'Employee'
}
],
defaults: {
age : 0,
fname : "",
lname : "",
manager : null
}
});
Specifying One-to-Many Relationship
var Location = Backbone.AssociatedModel.extend({
defaults: {
add1 : "",
add2 : null,
zip : "",
state : ""
}
});
var Project = Backbone.AssociatedModel.extend({
relations: [
{
type: Backbone.Many,
key: 'locations',
relatedModel:Location
}
],
defaults: {
name : "",
number : 0,
locations : []
}
});
Valid values for
relatedModel
A string (which can be resolved to an object type on the global scope), or a reference to a Backbone.AssociatedModel
type.
key
A string which references an attribute name on relatedModel
.
type : Backbone.One
or Backbone.Many
Used for specifying one-to-one or one-to-many relationships.
collectionType (optional) :
A string (which can be resolved to an object type on the global scope), or a reference to a Backbone.Collection
type. Determine the type of collections used for a Many
relation.
This tutorial demonstrates how to convert the following relationship graph into an AssociatedModels
representation
This image was generated via code.
var Location = Backbone.AssociatedModel.extend({
defaults:{
add1:"",
add2:null,
zip:"",
state:""
}
});
var Project = Backbone.AssociatedModel.extend({
relations:[
{
type:Backbone.Many,
key:'locations',
relatedModel:Location
}
],
defaults:{
name:"",
number:0,
locations:[]
}
});
var Department = Backbone.AssociatedModel.extend({
relations:[
{
type:Backbone.Many,
key:'controls',
relatedModel:Project
},
{
type:Backbone.Many,
key:'locations',
relatedModel:Location
}
],
defaults:{
name:'',
locations:[],
number:-1,
controls:[]
}
});
var Dependent = Backbone.AssociatedModel.extend({
validate:function (attr) {
return (attr.sex && attr.sex != "M" && attr.sex != "F") ? "invalid sex value" : undefined;
},
defaults:{
fname:'',
lname:'',
sex:'F',
age:0,
relationship:'S'
}
});
var Employee = Backbone.AssociatedModel.extend({
relations:[
{
type:Backbone.One,
key:'works_for',
relatedModel:Department
},
{
type:Backbone.Many,
key:'dependents',
relatedModel:Dependent
},
{
type:Backbone.One,
key:'manager',
relatedModel:'Employee'
}
],
validate:function (attr) {
return (attr.sex && attr.sex != "M" && attr.sex != "F") ? "invalid sex value" : undefined;
},
defaults:{
sex:'M',
age:0,
fname:"",
lname:"",
works_for:{},
dependents:[],
manager:null
}
});
CRUD operations on AssociatedModels trigger the appropriate Backbone system events. However, because we are working with an object graph, the event name now contains the fully qualified path from the source of the event to the receiver of the event. The remaining event arguments are identical to the Backbone event arguments and vary based on event type.
An update like this
emp.get('works_for').get("locations").at(0).set('zip', 94403);
can be listened to at various levels by spelling out the appropriate path
emp.on('change:works_for.locations[0].zip', callback_function);
emp.get('works_for').on('change:locations[0].zip', callback_function);
emp.get('works_for').get('locations').at(0).on('change:zip', callback_function);
With backbone v0.9.9 onwards, another object can also listen in to events like this
var listener = {};
_.extend(listener, Backbone.Events);
listener.listenTo(emp, 'change:works_for.locations[0].zip', callback_function);
listener.listenTo(emp.get('works_for'), 'change:locations[0].zip', callback_function);
listener.listenTo(emp.get('works_for').get('locations').at(0), 'change:zip', callback_function);
A detailed example is provided below to illustrate the behavior for other event types as well as the appropriate usage of the Backbone change-related methods used in callbacks.
This tutorial demonstrates the usage of eventing and change-related methods with AssociatedModels
Setup of relationships between AssociatedModel
instances
emp = new Employee({
fname:"John",
lname:"Smith",
age:21,
sex:"M"
});
child1 = new Dependent({
fname:"Jane",
lname:"Smith",
sex:"F",
relationship:"C"
});
child2 = new Dependent({
fname:"Barbara",
lname:"Ruth",
sex:"F",
relationship:"C"
});
parent1 = new Dependent({
fname:"Edgar",
lname:"Smith",
sex:"M",
relationship:"P"
});
loc1 = new Location({
add1:"P.O Box 3899",
zip:"94404",
state:"CA"
});
loc2 = new Location({
add1:"P.O Box 4899",
zip:"95502",
state:"CA"
});
project1 = new Project({
name:"Project X",
number:"2"
});
project2 = new Project({
name:"Project Y",
number:"2"
});
project2.get("locations").add(loc2);
project1.get("locations").add(loc1);
dept1 = new Department({
name:"R&D",
number:"23"
});
dept1.set({locations:[loc1, loc2]});
dept1.set({controls:[project1, project2]});
emp.set({"dependents":[child1, parent1]});
Assign Associated Model
instances to other properties
emp.on('change', function () {
console.log("Fired emp > change...");
});
emp.on('change:works_for', function () {
console.log("Fired emp > change:works_for...");
var changed = emp.changedAttributes();
});
emp.set({works_for:dept1});
Update attributes of AssociatedModel
instances
emp.off()
emp.get('works_for').on('change', function () {
console.log("Fired emp.works_for > change...");
});
emp.get('works_for').on('change:name', function () {
console.log("Fired emp.works_for > change:name...");
});
emp.on('change:works_for.name', function () {
console.log("Fired emp > change:works_for.name...");
});
emp.on('change:works_for', function () {
console.log("Fired emp > change:works_for...");
});
emp.get('works_for').set({name:"Marketing"});
Update an item in a Collection
of AssociatedModel
s
emp.get('works_for').get('locations').at(0).on('change:zip', function () {
console.log("Fired emp.works_for.locations[0] > change:zip...");
});
emp.get('works_for').get('locations').at(0).on('change', function () {
console.log("Fired emp.works_for.locations[0] > change...");
});
emp.get('works_for').on('change:locations[0].zip', function () {
console.log("Fired emp.works_for > change:locations[0].zip...");
});
emp.get('works_for').on('change:locations[0]', function () {
console.log("Fired emp.works_for > change:locations[0]...");
});
emp.on('change:works_for.locations[0].zip', function () {
console.log("Fired emp > change:works_for.locations[0].zip...");
});
emp.on('change:works_for.locations[0]', function () {
console.log("Fired emp > change:works_for.locations[0]...");
});
emp.on('change:works_for.controls[0].locations[0].zip', function () {
console.log("Fired emp > change:works_for.controls[0].locations[0].zip...");
});
emp.on('change:works_for.controls[0].locations[0]', function () {
console.log("Fired emp > change:works_for.controls[0].locations[0]...");
});
emp.get('works_for').on('change:controls[0].locations[0].zip', function () {
console.log("Fired emp.works_for > change:controls[0].locations[0].zip...");
});
emp.get('works_for').on('change:controls[0].locations[0]', function () {
console.log("Fired emp.works_for > change:controls[0].locations[0]...");
});
emp.get('works_for').get("locations").at(0).set('zip', 94403);
Add, remove and reset operations
emp.on('add:dependents', function () {
console.log("Fired emp > add:dependents...");
});
emp.on('remove:dependents', function () {
console.log("Fired emp > remove:dependents...");
});
emp.on('reset:dependents', function () {
console.log("Fired emp > reset:dependents...");
});
emp.get('dependents').on('add', function () {
console.log("Fired emp.dependents add...");
});
emp.get('dependents').on('remove', function () {
console.log("Fired emp.dependents remove...");
});
emp.get('dependents').on('reset', function () {
console.log("Fired emp.dependents reset...");
});
emp.get("dependents").add(child2);
emp.get("dependents").remove([child1]);
emp.get("dependents").reset();
The preceding examples corresponds to this test case.
Other examples can be found in the test suite.
For convenience, it is also possible to retrieve or set data by specifying a path to the destination (of the retrieve or set operation).
emp.get('works_for.controls[0].locations[0].zip')
emp.set('works_for.locations[0].zip', 94403);
When assigning a previously created object graph to a property in an associated model, care must be taken to query the appropriate object for the changed properties.
dept1 = new Department({
name:"R&D",
number:"23"
});
emp.set('works_for', dept1);
then inside a previously defined change
event handler
emp.on('change:works_for', function () {
});

Each operation comprises of n (10, 15, 20, 25, 30) inserts. The chart above compares the performance (time and operations/sec) of the two implementations. (backbone-associations v0.4.1 v/s backbone-relational v0.7.1)
Run tests on your machine configuration instantly here
Write your own test case here
- Support for backbone 0.9.10.
- Faster (Non-recursive) implementation of AssociatedModel change-related methods.
Version 0.4.0 - Diff
- Ability to perform set and retrieve operations with fully qualified paths.
Version 0.3.1 - Diff
- Bug fix for event paths involving collections at multiple levels in the object graph.
- Updated README with class diagram and example for paths involving collections.
Version 0.3.0 - Diff
- Added support for fully qualified event "path" names.
- Event arguments and event paths are semantically consistent.
- Now supports both backbone 0.9.9 and 0.9.2.
- New tutorials on usage. (part of README.md)
Version 0.2.0 - Diff
Added support for cyclic object graphs.
Version 0.1.0
Initial Backbone-associations release.