CanJS - Plugins - Define

canjs

What is the purpose of the define plugin?

The can.Map.define plugin allows you to finely control the behavior of attributes on a can.Map. For any attributes you declare in the define plugin, you can define the following:

  1. get
  2. set
  3. type
  4. value
  5. remove
  6. serialization

This plugin is a replacement for the now deprecated attributes and setter plugins.

How can we use the define plugin?

To use it, you specify an define object that is a mapping of properties to attribute definitions.

var Paginate = can.Map.extend(
    {
        define: {
            count: {
                type: "number",
                value: Infinity,
                // Keeps count above 0.
                set: function(newCount) {
                    return newCount < 0 ? 0 : newCount;
                }
            },
            offset: {
                type: "number",
                value: 0,
                // Keeps offset between 0 and count
                set: function(newOffset) {
                    var count = this.attr("count");
                    return newOffset < 0 ? 0 : Math.min(newOffset, !isNaN( count - 1) ? count - 1 : Infinity);
                }
            },
            limit: {
                type: "number",
                value: 5
            },
            page: {
                // Setting page changes the offset
                set: function(newVal){
                    this.attr('offset', (parseInt(newVal) - 1) * this.attr('limit'));
                },
                // The page value is derived from offset and limit.
                get: function (newVal) {
                    return Math.floor(this.attr('offset') / this.attr('limit')) + 1;
                }
            }
        }
    }
);

As you can see in the above code, define is an object, and within it, we define 4 properties. count, offset, limit, and page. And within each of these properties, we define the get, set, type, value attributes.

Using the a define plugin is as simple as adding a define property to the instance properties of the can.Map. This property is an object literal. Remember that passing in one argument to a can.Construct will set its instance properties. This is important to know should you create a can.Map that has both instance and static properties, and you want to use the define plugin.

//can.Map with one argument
var Person = can.Map.extend({
    define: {
        //define properties go here
        myProperty: {
            //property attributes
        }
    }
});

//can.Map with two arguments
var Person = can.Map.extend(
    {
        //static properties go here
    },
    {
        define: {
            //define properties go here
            myProperty: {
                //property attributes
            }
        }
    }
);

How can we listen to events on can.Observe objects using the define plugin?

One of the major advantages of using the define plugin in your applicaitons is that it handles managing the relationships of your observables for you. Any time you reference a can.Map or can.List (or one of their child objects) in a define property, that property is automatically subscribed as a listener for that can.Map or can.List.

define: {
    page: {
        set: function(newVal){
            //Because this setter function references the limit property, using .attr syntax
            //Any time that the limit property changes, this code will automatically run
            this.attr('offset', (parseInt(newVal) - 1) * this.attr('limit'));
        }
    }
}

What is the purpose of the get function of the define plugin?

The get function defines what happens when a value is read on a can.Map. It is typically used to provide properties that derive their value from other properties of the map, as below:

var Person = can.Map.extend({
    define: {
        fullName: {
            get: function () {
                return this.attr("first") + " " + this.attr("last");
            }
        }
    }
});

A function that specifies how the value is retrieved. The get function is converted to an async compute. It should derive its value from other values on the map. A get definition makes the property computed which means it will not be serialized by default.

What is the purpose of the set function of the define plugin?

A set function defines what happens when a value is set on a can.Map. It is typically used to update other attributes on the can.Map as a side effect, or coerce the set value into specific format. The setter function can take two optional arguments:

  1. newVal: The type function coerced value the user intends to set on the can.Map
  2. setVal: A callback that can set the value of the property asynchronously.

When using a setter function, the final value of the attribute is determined by the value the setter function returns. If the function returns a value, that value is used as the value of the attribute. If undefined is returned, the behavior depends on the number of arguments the setter declares, as below:

// If the setter does not specify the newValue argument,
// the attribute value is set to whatever was passed to attr.
set: function() { ... }

// If the setter specifies the newValue argument only,
// the attribute value will be removed
set: function(newValue) { ... }

// If the setter specifies both newValue and setValue, the value of
// the property will not be updated until setValue is called
set: function(newValue, setValue) { ... }

What is the purpose of the type property of the define plugin?

The type property converts a value passed to an attr setter function into a specific value type. The type can be specified as either a function, or one of the following strings:

  1. string - Converts the value to a string.
  2. date - Converts the value to a date or null if the date can not be converted.
  3. number - Passes the value through parseFloat.
  4. boolean - Converts falsey values (such as "" or 0) to false and everything else to true.
  5. * - Prevents the default coercion of Objects to can.Maps and Arrays to can.Lists.

There are two ways to define the type property:

  1. Type: {function()}
  2. type: {function (newValue, attrName) | String}

Type, uppercase, is instance specific. Using Type, a constructor will be invoked each time the property is set. Any data passed into the setter will be passed as arguments for the constructor. In contrast, type, lowercase, is set on the prototype of the object—i.e., it is not instance specific.

define: {
    items: {
        type: function(newValue){
            return typeof newValue === "string" ?  newValue.split(",") : newValue;
        }
    }
}
define: {
    count: {
        type: "number"
    },
    items: {
        type: function(newValue){
            if(typeof newValue === "string") {
                return newValue.split(",")
            } else if( can.isArray(newValue) ) {
                return newValue;
            }
        }
    }
}

The above code converts the count property to a number and the items property to an array.

What is the purpose of the value property of the define plugin?

Sets the default value for instances of the can.Map. If the default value should be an object of some type, it should be specified as the return value of a function, so that all instances of the map don't point to the same object. This is because JavaScript passes primitives by value, and all other values (objects, arrays, etc.) by reference.

define: {
    prop: {
        value: function(){ return []; }
    }
}

There are two ways to define the value property:

  1. Value: {function()}. Provides a constructor function, ensuring that a copy of the value is made for each instance. Specifies a function that will be called with new whose result is set as the initial value of the attribute.
  2. value: {function() | *}. Set on the prototype of the object—i.e., it is not instance specific.

Specifies the initial value of the attribute or a function that returns the initial value. For example, a default value of 0 can be specified like:

define: {
    prop: {
        value: 0
    }
}

Object types should not be specified directly on value because that same object will be shared on every instance of the Map. Instead, a value function that returns a fresh copy can be provided:

define: {
    prop: {
        value: function(){
            return {foo: "bar"}
        }
    }
}

The Value (with the uppercase V) property specifies a function that will be called with new whose result is set as the initial value of the attribute. For example, if the default value should be a can.List:

define: {
    prop: {
        Value: can.List
    }
}

In the above code, because can.List is a constructor function, we can use it with the Value property. Otherwise, we will have to specify a function.

What is the purpose of the remove property of the define plugin?

The remove function is called when an attribute is removed. Can be used, for example, for removal validation. A function that specifies what should happen when an attribute is removed with removeAttr. The following removes a modelId when makeId is removed:

define: {
    makeId: {
        remove: function(){
            this.removeAttr("modelId");
        }
    }
}

What is the purpose of the serialization property of the define plugin?

Defines how the attribute will behave when the map is serialized. Managing this property can be useful when serializing complex types like dates, arrays, or objects into strings. You can also control whether or not a given property can be serialized. Returning undefined from a serialization function for any property means this property will not be part of the serialized object.

define: {
    locationIds: {
        serialize: false
    }
}

By default, serialize does not include computed values. Properties with a get definition are computed and therefore are not added to the result. Non-computed properties values are serialized if possible and added to the result.

If true is specified, computed properties will be serialized and added to the result.

Paginate = can.Map.extend({
    define: {
        pageNum: { 
            get: function() { 
                return this.offset() / 20 
            },
            serialize: true
        }
    }
});

p = new Paginate({offset: 40});
p.serialize() //-> {offset: 40, pageNum: 2}

If false is specified, non-computed properties will not be added to the result. If a function is specified, the result of the function is added to the result.

How can we define default behavior for any unspecified attributes?

The can.Map.define plugin not only allows you to define individual attribute behaviors on a can.Map, but you can also define default behaviors that would apply to any unspecified attribute. This is particularly helpful for when you need a particular behavior to apply to every attribute on a can.Map but won't be certain of what every attribute will be.

The following example is a can.Map that is tied to can.route where only specified attributes that are serialized will be updated in the location hash:

var State = can.Map.extend(
    {
        define: {
            foo: {
                serialize: true
            },
            '*': {
                serialize: false
            }
        }
    }
);

var state = new State();

// tie State map to the route
can.route.map(state);
can.route.ready();

state.attr('foo', 'bar');
state.attr('bar', 'baz');
window.location.hash; // -> #!foo=bar

As you can see, we define default behavior for any unspecified attributes using the '*' wildcard.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License