CanJS - Map

canjs

What is the purpose of can.Map?

can.Map is an observable object / class. It provides a way for you to listen for and keep track of changes to objects. When you use the getters and setters provided by can.Map, events are fired that you can react to. can.Map also has support for working with deep properties.

How can we use can.Map?

To create an Observe, use new can.Map([props]). This will return a copy of props that emits events when its properties are changed with attr. You can read the values of properties on Observes directly, but you should never set them directly. You can also read property values using attr.

var aName = {a: 'Alexis'};
var map = new can.Map(aName);

// Observes are copies of data:
aName === map; // false

// reading from an Observe:
map.attr();    // {a: 'Alexis'}
map.a;         // 'Alexis'
map.attr('a'); // 'Alexis'

// setting an Observe's property:
map.attr('a', 'Alice');
map.a; // Alice

// removing an Observe's property;
map.removeAttr('a');
map.attr(); // {}

// Don't do this!
map.a = 'Adam'; // wrong!

The real power of maps comes from being able to react to properties being added, set, and removed. Maps emit events when properties are changed that you can bind to. can.Map has two types of events that fire due to changes on a map:

  1. the change event fires on every change to a map.
  2. an event named after the property name fires on every change to that property.
var o = new can.Map({});

o.bind('change', function(ev, attr, how, newVal, oldVal) {
    console.log('Something on o changed.');
});

o.bind('a', function(ev, newVal, oldVal) {
    console.log('a was changed.');
});

o.attr('a', 'Alexis'); // 'Something on o changed.'
                           // 'a was changed.'
o.attr({
    'a': 'Alice',      // 'Something on o changed.' (for a's change)
    'b': 'Bob'         // 'Something on o changed.' (for b's change)
});                    // 'a was changed.'

o.removeAttr('a');     // 'Something on o changed.'
                           // 'a was changed.'

In the above code, we created a can.Map object, and then we bind to the change event on the object, and bind to the change event on a specific property. The change event on the object is always fired first, followed by the change event on the specific property. When multiple attributes is changed at the same time, the change event on the object is fired multiple times.

What is the purpose of the delegate plugin?

It makes binding to specific types of events easier:

var o = new can.Map({});

o.delegate('a', 'add', function(ev, newVal, oldVal) {
    console.log('a was added.');
});

o.delegate('a', 'set', function(ev, newVal, oldVal) {
    console.log('a was set.');
});

o.delegate('a', 'remove', function(ev, newVal, oldVal) {
    console.log('a was removed.');
});

o.delegate('a', 'change', function(ev, newVal, oldVal) {
    console.log('a was changed.');
});

o.attr('a', 'Alexis'); // 'a was added.'
                           // 'a was changed.'

o.attr('a', 'Alice'); // 'a was set.'
                          // 'a was changed.'
o.removeAttr('a'); // 'a was removed.'
                       // 'a was changed.'

Why must we avoid using 'watch'?

Due to a method available on the base Object prototype called "watch", refrain from using properties with the same name on Gecko based browsers as there will be a collision.

What are deep properties?

attr can also set and read deep properties. All you have to do is specify the property name as you normally would:

var people = new can.Map({names: {}});

// set a property:
people.attr('names.a', 'Alice');

// get a property:
people.attr('names.a'); // 'Alice'
people.names.attr('a'); // 'Alice'

// get all properties:
people.attr(); // {names: {a: 'Alice'}}

Objects that are added to Observes become Observes themselves behind the scenes, so changes to deep properties fire events at each level, and you can bind at any level. As this example shows, all the same events are fired no matter what level you call attr at:

var people = new can.Map({names: {}});

people.bind('change', function(ev, attr, how, newVal, oldVal) {
    console.log('people change: ' + attr + ', ' + how + ', ' + newVal + ', ' + oldVal);
});

people.names.bind('change', function(ev, attr, how, newVal, oldVal) {
    console.log('people.names change' + attr + ', ' + how + ', ' + newVal + ', ' + oldVal);
});

people.bind('names', function(ev, newVal, oldVal) {
    console.log('people names: ' + newVal + ', ' + oldVal);
});

people.names.bind('a', function(ev, newVal, oldVal) {
    console.log('people.names a: ' + newVal + ', ' + oldVal);
});

people.bind('names.a', function(ev, newVal, oldVal) {
    console.log('people names.a: ' + newVal + ', ' + oldVal);
});

people.attr('names.a', 'Alice'); // people change: names.a, add, Alice, undefined
                                  // people.names change: a, add, Alice, undefined
                                  // people.names a: Alice, undefined
                                  // people names.a: Alice, undefined
people.names.attr('b', 'Bob');   // people change: names.b, add, Bob, undefined
                                  // people.names change: b, add, Bob, undefined
                                  // people.names b: Bob, undefined
                                  // people names.b: Bob, undefined

As shown above, attr enables reading and setting deep properties so special care must be taken when property names include dots '.'. To read a property containing dots, escape each one using '\'. This prevents attr from performing a deep lookup and throwing an error when the deep property is not found.

var person = new can.Map({
    'first.name': 'Alice'
});
person.attr('first.name'); // throws Error
person.attr('first\.name'); // 'Alice'

When setting a property containing dots, pass an object to attr containing the property name and new value. Setting a property by passing a string to attr will attempt to set a deep property and will throw an error.

var person = new can.Map({
    'first.name': 'Alice'
});
person.attr('first.name', 'Bob'); // throws Error
person.attr('first\.name', 'Bob'); // throws Error
person.attr({'first.name': 'Bob'}); // Works

What is the purpose of can.LazyMap?

Just like can.Map, can.LazyMap provides a way to listen for and keep track of changes to objects. But unlike Map, a LazyMap only initializes data when bound, set or read. For lazy observable arrays, can.LazyList is also available. This on demand initialization of nested data can yield big performance improvements when using large datasets that are deeply nested data where only a fraction of the properties are accessed or bound to.

What are the limitations of can.LazyMap?

Although passing all original can.Map and can.List tests, can.LazyMap and can.LazyList do not work with the attributes, setter, delegate, backup and validations plugins. Additionally, If all properties of a LazyMap or LazyList are being read, bound or set, initialization time can be slightly higher than using a Map or List.

What is the purpose of the can.Map.backup plugin?

The can.Map.backup is a plugin that provides a dirty bit for properties on an Map, and lets you restore the original values of an Map's properties after they are changed. Here is an example showing how to use backup to save values, restore to restore them, and isDirty. To check if the Map has changed:

var recipe = new can.Map(
    {
        title: 'Pancake Mix',
        yields: '3 batches',
        ingredients: [
            {
                ingredient: 'flour',
                quantity: '6 cups'
            },{
                ingredient: 'baking soda',
                quantity: '1 1/2 teaspoons'
            },{
                ingredient: 'baking powder',
                quantity: '3 teaspoons'
            },{
                ingredient: 'salt',
                quantity: '1 tablespoon'
            },{
                ingredient: 'sugar',
                quantity: '2 tablespoons'
            }
        ]
    }
);

recipe.backup();
recipe.attr('title', 'Flapjack Mix');
recipe.title;     // 'Flapjack Mix'
recipe.isDirty(); // true
recipe.restore();
recipe.title;     // 'Pancake Mix'
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License