CanJS - Observables

canjs

What is can.Observe?

can.Observe provides the observable pattern for JavaScript Objects (and lists).

var person = new can.Observe({ name: "josh"});

person.bind("name", function(ev, newVal, oldVal){
  newVal; // "Josh Dean"
  oldVal; // "josh"
});

person.attr("name"); // "josh"
person.name; // "josh"
person.attr("name","Josh Dean");

You can set and remove property values on objects, listen for property changes, and work with nested properties. can.Observe is used by both can.Model and can.route.

What are the components that inherit from can.Observe?

  1. can.Map
  2. can.List
  3. can.compute
  4. can.Model
  5. can.route
  6. can.Component

can.Map and can.List are often extended to create observable types. can.Models and can.route are based on can.Map. can.Component's scope is a can.Map

How can we create a can.Map?

To create a Map, call new can.Map(obj). This will give you a map with the same properties and values as obj.

var pagination = new can.Map({page: 1, perPage: 25, count: 1388});
pagination.attr('perPage'); // 25

How can we create a can.List?

To create a List, call new can.List(array). This will give you a List with the same elements as array.

var hobbies = new can.List(['programming', 'bball', 'party rocking']);
hobbies.attr(2); // 'partying'

What is the purpose of the attr method?

The attr method is used to read and write a property or properties from a Map or List.

pagination.attr('perPage'); // 25
pagination.attr('perPage', 50);
pagination.attr('perPage'); // 50

What happens if the attr method is invoked with an object or without any parameter?

pagination.attr({page: 10, lastVisited: 1});
pagination.attr(); // {page: 10, perPage: 50, count: 1388, lastVisited: 1}

When called with an object, the object that was specified as parameters to the attr method is merged into the object that the attr method belong to. When invoked without any parameter, the attr method returns a collection of all the properties in this can.Map.

How can we remove a property from a can.Map object?

Properties can be removed from Observes with removeAttr, which is equivalent to the delete keyword:

pagination.removeAttr('count');
pagination.attr(); // {page: 10, perPage: 50, lastVisited: 1}

How can we observe events?

When a property on a Map is changed with attr, the Map will emit two events: A change event and an event with the same name as the property that was changed. You can listen for these events by using bind:

paginate.bind('change', function(event, attr, how, newVal, oldVal) {
    attr; // 'perPage'
    how; // 'set'
    newVal; // 30
    oldVal; // 50
});

paginate.bind('perPage', function(event, newVal, oldVal) {
    newVal; // 30
    oldVal; // 50
});
paginate.attr('perPage', 30);

When an attribute is change, why does CanJS fire two events?

When a property on a Map is changed with attr, the Map will emit two events: A change event and an event with the same name as the property that was changed. I am not really sure on this, but this may be useful.

How can we stop observing events?

You can similarly stop listening to these events by using unbind:

var timesChanged = 0,
changeHandler = function() { timesChanged++; },
obs = new can.Map({value: 10});

obs.bind('change', changeHandler);
obs.attr('value', 20);
timesChanged; // 1

obs.unbind('change', changeHandler);
obs.attr('value', 30);
timesChanged; // 1

In the above code, changeHandler is just a normal JavaScript function which update the timesChanged variable (which is just a normal JavaScript variable, and is not an attribute of the observable object). The changeHandler function is then passed to bind and unbind.

How can we iterate through the properties of a Map?

If you want to iterate through the properties on a Map, use each:

paginate.each(function(val, key) {
    console.log(key + ': ' + val);
});

How can we extend can.Map or can.List to create custom observable types?

Extending a can.Map (or can.List) lets you create custom observable types. The following extends can.Map to create a Paginate type that has a .next() method to change its state:

Paginate = can.Map.extend({
    limit: 100,
    offset: 0,
    count: Infinity,
    page: function() {
        return Math.floor(this.attr('offset') / this.attr('limit')) + 1;
    },
    next: function() {
        this.attr('offset', this.attr('offset') + this.attr('limit') );
    }
});

var pageInfo = new Paginate();
pageInfo.attr("offset") //-> 0
pageInfo.next();
pageInfo.attr("offset") //-> 100
pageInfo.page() //-> 2

What are the methods available to a can.List object?

  1. indexOf, which looks for an item in a List.
  2. pop, which removes the last item from a List.
  3. push, which adds an item to the end of a List.
  4. shift, which removes the first item from a List.
  5. unshift, which adds an item to the front of a List.
  6. splice, which removes and inserts items anywhere in a List.

What events are fired when the can.List methods are invoked?

When these methods are used to modify a List, the appropriate events are emitted. See the API for Lists for more information on the arguments passed to those event handlers.

What is can.compute?

CanJS also provides a way to make values themselves observable with can.compute. A Compute represents a dynamic value that can be read, set, and listened to just like a Map.

What is a static compute?

A simple static Compute contains a single value, and is created by calling can.compute(value). This value can be read, set, and listened to:

// create a Compute
var age = can.compute(25),
previousAge = 0;

// read the Compute's value
age(); // 25

// listen for changes in the Compute's value
age.bind('change', function(ev, newVal, oldVal) {
    previousAge = oldVal;
});

// set the Compute's value
age(26);
age(); // 26
previousAge; // 25

In the above code, the result of calling can.compute(25) is actually a function which encapsulate the variable that hold the value 25. To change the value of this variable, we invoke age(26). To retrieve the value currently being hold by this variable, we invoke age() without any parameter.

What are composite computes?

Computes can also be used to generate a unique value based on values derived from other observable properties. This type of compute is created by calling can.compute(getterFunction). When the observable properties that the compute is derived from change, the value of the compute changes:

var name = new can.Map({
    first: 'Alice',
    last: 'Liddell'
});
var fullName = can.compute(function() {
    // We use attr to read the values
    // so the compute knows what to listen to.
    return name.attr('first') + ' ' + name.attr('last');
});

var previousName = '';

fullName(); // 'Alice Liddell'

fullName.bind('change', function(ev, newVal, oldVal) {
    previousName = oldVal;
});

name.attr({
    first: 'Allison',
    last: 'Wonderland'
});

fullName(); // 'Allison Wonderland'
previousName; // 'Alice Liddell'

In the above code, name is a can.Map object, and fullName is composite compute that is derived by the provided function. You can bind to the change event off the fullName compute, and when name is changed, the fullName compute is automatically updated.

What are Converted Computes?

Computes are also useful for creating links to properties within Observes. One of the most frequent examples of this is when converting from one unit to another.

// progress ranges from 0 to 1.
var project = new can.Map({ progress: 0.3 });
var progressPercentage = can.compute(function(newVal) {
    if(newVal !== undefined) {
        // set a value
        project.attr('progress', newVal / 100);
    } else {
        // get the value
        return project.attr('progress') * 100;
    }
});

progressPercentage(); // 30

// Setting percentage...
progressPercentage(75);

// ...updates project.progress!
project.attr('progress'); // .75

In the above code, project is a can.Map object, which currently has the 'progress' attribute set at 0.3, and progressPercentage is a converted compute which when invoked without any parameter, it convert the existing value into percentage (0.3 -> 30%), and when invoke with a value such as progressPercentage(75), it updates the internal value (75 -> 0.75). Notice that, when the value of the converted compute is changed, the property of the underlying map object is changed via the function that was used to create the compute.

How does can.Observe works with nested data?

It converts nested objects into observes automatically. For example:

var person = new can.Observe({
    name: { first: 'Justin', last: 'Meyer' },
    hobbies: [ 'programming', 'party rocking' ]
})
person.attr( 'name.first' ) //-> 'Justin'
person.attr( 'hobbies.0' ) //-> 'programming'

How does CanJS handle nested property changes?

The change events bubble, letting observes listen for when a nested property changes:

person.bind( 'change', function( ev, attr, how, newVal, oldVal ) {
    attr //-> 'name.last'
    how //-> 'set'
    newVal //-> 'Meyer'
    oldVal //-> 'Myer'
    });
person.attr( 'name.last', 'Meyer' );
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License