Observables

  • page
Observables  

CanJS's observables - can.Map, can.List, and can.compute - let you make changes to data and listen to those changes. Observables are the subjects in the observer pattern. CanJS comes with three forms of observables:

can.Map and can.List are often extended to create observable types. Models and can.route are based on can.Map, and can.Component's scope is a can.Map, but observables are useful on their own too.

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

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

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

Manipulating properties

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

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

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}

Listening to 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);

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

Iterating though a Map

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

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

// The console shows:
// page: 10
// perPage: 30
// lastVisited: 1

Extending a Map

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

Observable Arrays

As mentioned above, CanJS also provides observable arrays with can.List. can.List inherits from can.Map, so a List works much the same way an Map does, with the addition of several methods useful for working with arrays:

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

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.

Computed values

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.

Static Computes

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

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'

Since the value of the Compute is cached any time a derived value is changed, reading the value is fast.

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

percentage();     // 30

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