Observables

  • page
Observables  


In this Chapter

  • can.Map, and
  • can.List

Observables are the subjects in the observer pattern. They let you create relationships between objects where one object (or objects) listens for and responds to changes in another object. Most of the core objects in CanJS are observables. Understanding how to effectively work with observables lies at the heart of understanding how to build successful CanJS applications.

In this section, we’ll review the two observables that make up the core of most CanJS objects:

can.Map and can.List are often extended to create observable types. For example, can.Model and can.route are based on can.Map, and a can.Component’s viewModel is a can.Map.

Creating Instances

To create a Map, call new can.Map(object). This will give you a map with the same properties and values as the object you passed in to the can.Map constructor.

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

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); // 'party rocking'

Manipulating properties

The attr method is used to read a property from, or write a property to a can.Map or can.List. While you can read the properties of a can.Map or can.List directly off of the object, to take advantage of the observable functionality you must use the .attr syntax.

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

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 by using removeAttr, which is equivalent to the delete keyword:

pagination.removeAttr('count');
pagination.attr(); // {page: 10, perPage: 50, 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:

Paginate = can.Map.extend({
  define: {
    limit: {
      value: 100
    },
    offset: {
      value: 100
    },
    count: {
      value: Infinity
    }
  },
  next: function() {
    this.attr('offset', this.attr('offset') + this.attr('limit') );
  }
});

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

pageInfo.next();

pageInfo.attr("offset") //-> 200

Responding to changes

When a property on a Map is changed with attr, it will emit an event with the name of the changed property. You can bind to those events and perform some action:

pagination.bind('page', function(event, newVal, oldVal) {
    newVal; // 11
    oldVal; // 10
    
    $("#page").text("Page: "+newVal);
});

pagination.attr('page', 11);

Although bind and its corresponding unbind method exist, there's almost no reason to ever use them! This is because there are better ways to perform the common actions that would require binding to an observable.

For example, stache templates will automatically update when an observable changes:

var template = can.stache("<span id='page'>{{page}}</span>");
$("body").append(template(pagination));

document.getElementById("page").innerHTML //-> "11"

pagination.attr('page', 12);

document.getElementById("page").innerHTML //-> "12"

The other common use case is to create some new, derived value. can.compute or define getters lets you use functional (and reactive) programming techniques to derive new values from source state.

For example, we can create a virtual page observable that derives its value from the offset and limit:

var pagination = new Paginate({
  limit: 10,
  offset: 20
});

var page = can.compute(function(){
  return Math.floor(pagination.attr('offset') / 
                    pagination.attr('limit')) + 1;
});

page() //-> 3

page.bind("change", function(ev, newValue){
  newValue //-> 4
});

pagination.attr("offset",30);

In this example page will automatically be updated when either offset or limit change.

However, page is more commonly created as a "virtual" property of the Paginate Map type using a define getter:

Paginate = can.Map.extend({
  define: {
    ...
    page: {
      get: function() {
        return Math.floor(this.attr('offset') / this.attr('limit')) + 1;
      }
    }
  },
  ...
});

var pageInfo = new Paginate({
  limit: 10,
  offset: 20,
  count: 30
});
pageInfo.attr("page") //-> 3

pageInfo.bind("page", function(ev, newVal){
  newVal //-> 4
});

pageInfo.next();

Using computes and define getters are very similar to using functional-reactive programming event streams. Given some source state, they are able to derive and combine it into new values.

Computes and define getters are easier, but less powerful than event streams. Computes and define getters only respond to changes in values where event streams are also able to respond to events. However, computes and define getters are eaiser to express and automatically manage subscriptions to source values.

For example, consider deriving a total for one of two menus depending on the time of day:

var lunch = new can.List([
  {name: "nachos", price: 10.25},
  {name: "water", price: 0},
  {name: "taco", price: 3.25}
]);

var dinner = new can.List([
  {name: "burrito", price: 12.25},
  {name: "agua", price: 1.20}
]);

var timeOfDay = can.compute("lunch");

var total = can.compute(function(){
  var list = timeOfDay() === "lunch" ? lunch : dinner;
  var sum = 0;
  list.forEach(function(item){
    sum += item.attr("price");
  });
  return sum;
});

In this example, total will listen to not only timeOfDay, but also when items are added or removed to lunch or dinner, and each item's price. Furthermore, it only listens to just what needs to be listened to. It will listen to either lunch or dinner, but not both.

In the next chapter we'll expand on the use of the define plugin and show how it can handle asyncronous derived data like AJAX requests.

Iterating though a Map

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

var pagination = new can.Map({page: 10, perPage: 25, count: 1388});

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

// The console shows:
// page: 10
// perPage: 25
// count: 1388

Observable Arrays

CanJS also provides observable arrays with can.List. can.List inherits from can.Map. A can.List works much the same way a can.Map does, with the addition of 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 events are emitted that you can listen for, as well. See the API for Lists for more information.

‹ Application Foundations The Define Plugin ›