Typically Backbone Applications will store application-relevant object graphs in a context (either application-wide or local) which will be referred to by other entities (views typically, but possibly even other models) in the application as the source of data. It therefore becomes very important to provide a mechanism to listen to any kind of create-update-remove operations happening on these graphs. Backbone-Associations piggy-backs on the standard backbone events to provide applications a way to stay tuned to these updates. However, with an object graph, the use of a fully qualified path name to specify an event name is more appropriate. A fully qualified event name simply specifies the path from the source of the event to the receiver of the event. The fully qualified event name reduces to the regular Backbone event names for Backbone Models (single node graphs). The remaining event arguments are identical to the Backbone event arguments and vary based on event type(change,add remove etc). Backbone-Associations also honours the Backbone change-related state API. This means that previous and changed attributes will continue to work as expected with CRUD operations on the object graph.
An update like thisemp.get('works_for').get("locations").at(0).set('zip', 94403);can be listened to at various levels (in the object hierarchy) by spelling out the appropriate path
emp.on('change:works_for.locations[0].zip', callback_function); //Fully qualified event name is 'works_for.locations[0].zip' emp.on('change:works_for.locations[*]', callback_function); //Fully qualified event name is 'works_for.locations[*]' emp.get('works_for').on('change:locations[0].zip', callback_function); //Fully qualified event name is 'locations[0].zip' emp.get('works_for').get('locations').at(0).on('change:zip', callback_function); //Fully qualified event name is 'zip'With Backbone v0.9.9+ 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);
Backbone-Associations only changes the event name (of standard Backbone events) to a fully qualified event name. Beyond that, the regular Backbone events, their arguments and the change-related methods should continue to work as usual. Additionally, Backbone-associations introduces a new event - nested-change - which becomes relevant in an object graph scenario.
emp.get('works_for').get("locations").at(0).set('zip', 94403);
emp could listen in to changes happening in locations in these possible ways
//Listen to a specific item in the collection emp.on('change:works_for.locations[0]', callback_function)
//Listen to changes in any item in the collection. The arguments will contain the changed item emp.on('change:works_for.locations[*]', callback_function)
//Listen to a specific attribute in a specific item in the collection emp.on('change:works_for.locations[0].zip', callback_function)
//Listen to a specific attribute in any item in the collection emp.on('change:works_for.locations[*].zip', callback_function)
emp.get('works_for').set({name:"Marketing"});
emp could listen in to changes happening in name
//Listen to any change in works_for attribute emp.on('change:works_for', callback_function)
//Listen to changes in a specific attribute of works_for emp.on('change:works_for.name', callback_function)
//Listen to changes in any child Model or Collection at any depth emp.on('nested-change', callback_function)
//Will NOT fire. See nested-change event documentation for why this is so emp.on('change', callback_function)
In a single node graph (like Backbone), a change in any attribute would trigger the "change" event. However when model attributes can be other Models (and Collections), a property change in a contained Model would not trigger a "change" event in the parent. This is because the reference to the contained Model has not really changed (in the sense of a memory replacement). This can be limiting if the intention is to listen in to any "change" event in the entire object graph (without specifying fully qualified paths) at a higher level (like the root of the hierarchy). The nested-change event was born to address this flow. This will be useful in scenarios where it is just important to know that something in sub hierarchies has changed.
Consider this example to make it clear//Listen to changes in *any* child Model or Collection at any depth emp.on('nested-change', function () { //arguments[0] > "works_for.controls[0].locations[0]" //arguments[1] > emp.get("works_for.controls[0].locations[0]") }); //Will NOT fire. emp.on('change', callback_function) emp.get('works_for').get("locations").at(0).set('zip', 94403);
Arguments | Description |
0 | The full path to the changed object |
1 | The changed object |
v0.6.0+ nested-change event is switched OFF by default for performance reasons. They can be turned on by setting Backbone.Associations.EVENTS_NC = true anytime in the application flow.
emp.get('works_for.controls[1].locations').add({ id:3, add1:"loc3" });emp can tune in to add events by specifying a path like this
emp.on('add:works_for.controls[1].locations', function () { // Add action happened for location collection inside the sub-graph rooted at controls[1] }); emp.on('add:works_for.controls[*].locations', function () { // Add action happened for location collection inside any control sub-graphs });
emp.get('works_for.controls[0].locations').remove(loc2);emp can tune in to remove events by specifying a path like this
emp.on('remove:works_for.controls[0].locations', function () { //Listen to remove changes in the locations collection rooted at controls[0] }); emp.on('remove:works_for.controls[*].locations', function () { //Listen to remove changes in the locations collection rooted inside any `controls` sub-graph });
emp.get('works_for.controls[0].locations').reset();emp can tune in to reset events by specifying a path like this
emp.on('reset:works_for.controls[0].locations', function () { //Listen to reset changes in the locations collection rooted at controls[0] }); emp.on('reset:works_for.controls[*].locations', function () { //Listen to reset changes in the locations collection rooted inside any `controls` sub-graph });
locCol.comparator = function(l){ return l.get("state"); }; emp.get('works_for.controls[0].locations').sort();emp can tune in to sort events by specifying a path like this
emp.on('sort:works_for.controls[0].locations', function(){ //Listen to sort changes in the locations collection rooted at controls[0] }); emp.on('sort:works_for.controls[*].locations', function(){ //Listen to sort changes in the locations collection rooted inside any `controls` sub-graph });
loc = emp.get('works_for.controls[0].locations[0]'); loc.destroy();emp can tune in to destroy events by specifying a path like this
emp.on("destroy:works_for.controls[0].locations", function(){ //Listen to destroy events in the locations collection rooted at controls[0] }); emp.on("destroy:works_for.controls[*].locations", function(){ //Listen to destroy events in the locations collection rooted inside any `controls` sub-graph });
Backbone-Associations also honours the Backbone change-related state API. This means that previous and changed attributes will continue to work as expected with CRUD operations on the object graph.
emp.on('change:works_for', function () { console.log("Fired emp > change:works_for.name..."); //emp.get("works_for").hasChanged() === true; //emp.hasChanged() === true; //emp.hasChanged("works_for") === true; //emp.changedAttributes()['works_for'].toJSON() equals emp.get("works_for").toJSON(); //emp.get("works_for").previousAttributes()["name"] === "R&D"; //emp.get("works_for").previous("name") === "R&D"; }); emp.get('works_for').set({name:"Marketing"});
var dept = new Department({ name:"R&D", number:"23", id:1 }); emp.set('works_for',dept); emp.get('works_for').on("change", function () { //Comes here because the dept has been updated equal(emp.get('works_for').get('name'), "R&D++"); }); // Setting a department with the same id causes an update emp.set('works_for',{id:1, name:"R&D++"}); //emp.get('works_for') === dept; //Should not trigger event in emp.get('works_for').on("change", callback) as we have a diff dept id (and instance) emp.set('works_for', {id:3, name:"A new department name"}); //emp.get('works_for') !== dept;Note : Earlier versions of Backbone-associations used to blindly create new instances; causing previously bound event handlers to not fire. This resulted in additional application complexity. This problem has now been rectified in v0.5.0+
Employee = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'manager', relatedModel:'Employee' } ], defaults:{ fname:"", lname:"", manager:undefined }, });The next example assigns the manager to himself. (The case of the company owner)
var owner = new Employee({'fname':'Jack', 'lname':'Welch'}); owner.set({'manager':owner});Since Backbone-associations API are cycle aware, eventing will not loop indefinitely. Ditto for toJSON, clone APIs. The next example demonstrates eventing with self-references.
var owner = new Employee({'fname':'Jack', 'lname':'Welch'}); owner.on('change:manager', function () { console.log("emp > `change:manager` fired..."); }); owner.set({'manager':owner}); //Console log //emp : emp > `change:manager` fired... //manager (who is also an emp) : emp > `change:manager` fired... owner.get("manager").on('change', function () { console.log("manager > `change` fired..."); }); owner.get('manager').set({'fname':'Jack Sr.'}); //Console log //emp > `change:manager` fired... //manager > `change` fired... //Both should have the same name as they are identical objects owner.get('fname') == "Jack Sr." owner.get('manager').get('fname') == "Jack Sr."
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" }); //dept1.hasChanged() === false; emp.set('works_for', dept1);Then inside a previously defined change event handler
emp.on('change:works_for', function () { //emp.get('works_for').hasChanged() === false; as we query a previously created `dept1` instance //emp.hasChanged('works_for') === true; as we query emp whose 'works_for' attribute has changed });
This extension makes use of fully-qualified-event-path names to identify the location of the change in the object graph. (And the event arguments would have the changed object or object property). The unqualified change event would work if an entire object graph is being replaced with another. For example
emp.on('change', function () { //This WILL fire }); emp.on('change:works_for', function () { //This WILL fire }); emp.set('works_for', {name:'Marketing', number:'24'});However, if attributes of a nested object are changed, the unqualified change event will not fire for objects (and their parents) who have that nested object as their child.
emp.on('change', function () { //This will NOT fire }); emp.on('change:works_for', function () { //This WILL fire because something in works_for has changed }); emp.get('works_for').set('name','Marketing');To listen to changes in sub-hierarchies at a higher level (like emp in this case), use the nested-change instead
emp.on('nested-change', function () { //This will fire //args[0] > works_for //args[1] > emp.get('works_for') });