Try out our recently released Firebug extension - Backbone Eye : Understand Backbone application behavior without debugging JavaScript!

Introduction to Events


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 this
    emp.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);
    

Event Catalogue


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.

"change:[path]"
path to the nested Model or Collection which caused the update. This can be easily understood with examples
  • Scenario I : Source of update is an item in a collection
    emp.get('works_for').get("locations").at(0).set('zip', 94403);
                                

    emp could listen in to changes happening in locations in these possible ways

    1. //Listen to a specific item in the collection
      emp.on('change:works_for.locations[0]', callback_function)
                                              
    2. //Listen to changes in any item in the collection. The arguments will contain the changed item
      emp.on('change:works_for.locations[*]', callback_function)
                                              
    3. //Listen to a specific attribute in a specific item in the collection
      emp.on('change:works_for.locations[0].zip', callback_function)
                                              
    4. //Listen to a specific attribute in any item in the collection
      emp.on('change:works_for.locations[*].zip', callback_function)
                                              

  • Scenario II : Source of update is an AssociatedModel
    emp.get('works_for').set({name:"Marketing"});
                                

    emp could listen in to changes happening in name

    1. //Listen to any change in works_for attribute
      emp.on('change:works_for', callback_function)
                                              
    2. //Listen to changes in a specific attribute of works_for
      emp.on('change:works_for.name', callback_function)
                                              
    3. //Listen to changes in any child Model or Collection at any depth
      emp.on('nested-change', callback_function)
                                              
    4. //Will NOT fire. See nested-change event documentation for why this is so
      emp.on('change', callback_function)
                                              

"nested-change"

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.

add:[path]
path to the nested Collection where the item was added. This can be easily understood with an example.

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
});
                        

remove:[path]
path to the nested Collection where the item was removed. This can be easily understood with an example.

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
});
                        

reset:[path]
path to the collection which was reset. This can be easily understood with an example.

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
});
                        

sort:[path]
path to the collection which was sorted. This can be easily understood with an example.

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
});

                        

destroy:[path]
path to the collection which was destroyed. This can be easily understood with an example.

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
});
                        

Change related API


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"});
            

Intelli-update : Create or update objects based on idAttribute value


From v0.5.0, Backbone-associations will use the idAttribute value to determine whether to update a nested object or to create a new one. The example below should make this clear.
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+

Self References a.k.a Cycles


Backbone-associations supports cyclic graphs. Consequently, it allows relatedModels to point to themselves. This could be useful in composition scenarios - where the composed object is itself the container.

The example below shows how to specify self-references during model definition.
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."
 

Pitfalls


  1. Query the appropriate object to determine change

    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
    });
                    


  2. Use unqualified change event name with care

    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')
    });
                    

Next Steps


  1. Go through a comprehensive tutorial.
  2. See real-world recipes contributed by users.