can-define/map/map
Create observable objects.
new DefineMap([props])
The can-define/map/map
module exports the DefineMap
constructor function.
Calling new DefineMap(props)
creates a new instance of DefineMap or an extended DefineMap. Then, assigns every property on props
to the new instance. If props are passed that are not defined already, those property definitions are created. If the instance should be sealed, it is sealed.
var DefineMap = require("can-define/map/map");
var person = new DefineMap({
first: "Justin",
last: "Meyer"
})
Custom DefineMap
types, with special properties and behaviors, can be defined with extend.
Parameters
- props
{Object}
:Properties and values to seed the map with.
Use
can-define/map/map
is used to create easily extensible observable types with well defined
behavior.
For example, a Todo
type, with a name
property, completed
property, and a toggle
method, might be defined like:
var DefineMap = require("can-define/map/map");
var Todo = DefineMap.extend({
name: "string",
completed: {type: "boolean", value: false},
toggle: function(){
this.completed = !this.completed;
}
})
The Object passed to .extend
defines the properties and methods that will be
on instances of a Todo
. There are a lot of ways to define properties. The
PropDefinition type lists them all. Here, we define:
name
as a property that will be type coerced into aString
.completed
as a property that will be type coerced into aBoolean
with an initial value offalse
.
This also defines a toggle
method that will be available on instances of Todo
.
Todo
is a constructor function. This means instances of Todo
can be be created by
calling new Todo()
as follows:
var myTodo = new Todo();
myTodo.name = "Do the dishes";
myTodo.completed //-> false
myTodo.toggle();
myTodo.completed //-> true
You can also pass initial properties and their values when initializing a DefineMap
:
var anotherTodo = new Todo({name: "Mow lawn", completed: true});
myTodo.name = "Mow lawn";
myTodo.completed //-> true
Declarative properties
Arguably can-define
's most important ability is its support of declarative properties
that functionally derive their value from other property values. This is done by
defining getter properties like fullName
as follows:
var Person = DefineMap.extend({
first: "string",
last: "string",
fullName: {
get : function(){
return this.first + " " + this.last;
}
}
});
fullName
can also be defined with the ES5 shorthand getter syntax:
var Person = DefineMap.extend({
first: "string",
last: "string",
get fullName(){
return this.first + " " + this.last;
}
});
Now, when a person
is created, there is a fullName
property available like:
var me = new Person({first: "Harry", last: "Potter"});
me.fullName //-> "Harry Potter"
This property can be bound to like any other property:
me.on("fullName", function(ev, newValue, oldValue){
newValue //-> Harry Henderson
oldValue //-> Harry Potter
});
me.last = "Henderson";
getter
properties use can-compute internally. This means that when bound,
the value of the getter
is cached and only updates when one of its source
observables change. For example:
var Person = DefineMap.extend({
first: "string",
last: "string",
get fullName(){
console.log("calculating fullName");
return this.first + " " + this.last;
}
});
var hero = new Person({first: "Wonder", last: "Woman"});
// console.logs "calculating fullName"
hero.fullName //-> Wonder Woman
// console.logs "calculating fullName"
hero.fullName //-> Wonder Woman
// console.logs "calculating fullName"
hero.on("fullName", function(){});
hero.fullName //-> "Wonder Woman"
// console.logs "calculating fullName"
hero.first = "Bionic"
// console.logs "calculating fullName"
hero.last = "Man"
hero.fullName //-> "Bionic Man"
If you want to prevent repeat updates, use can-event/batch/batch:
hero.fullName //-> "Bionic Man"
var canBatch = require("can-event/batch/batch");
canBatch.start();
hero.first = "Silk";
hero.last = "Spectre";
// console.logs "calculating fullName"
canBatch.stop();
Asynchronous getters
getters
can also be asynchronous. These are very useful when you have a type
that requires data from the server. This is very common in can-component
view-models. For example, a view-model
might take a todoId
value, and want
to make a todo
property available:
var ajax = require("can-util/dom/ajax/ajax");
var TodoViewModel = DefineMap.extend({
todoId: "number",
todo: {
get: function(lastSetValue, resolve){
ajax({url: "/todos/"+this.todoId}).then(resolve)
}
}
});
Asynchronous getters only are passed a resolve
argument when bound. Typically in an application,
your template will automatically bind on the todo
property. But to use it in a test might
look like:
var fixture = require("can-fixture");
fixture("GET /todos/5", function(){
return {id: 5, name: "take out trash"}
});
var todoVM = new TodoViewModel({id: 5});
todoVM.on("todo", function(ev, newVal){
assert.equal(newVal.name, "take out trash");
});
Getter limitations
There's some functionality that a getter or an async getter can not describe declaratively. For these situations, you can use set or even better, use the can-define-stream plugin.
For example, consider a state and city locator where you pick a United States state like Illinois and then a city like Chicago. In this example, we want to clear the choice of city whenever the state changes.
This can be implemented with set like:
Locator = DefineMap.extend({
state: {
type: "string",
set: function(){
this.city = null;
}
},
city: "string"
});
var locator = new Locator({
state: "IL",
city: "Chicago"
});
locator.state = "CA";
locator.city //-> null;
This isn't declarative anymore because changing state imperatively changes
the value of city
. The can-define-stream plugin can make this functionality
entirely declarative.
var Locator = DefineMap.extend({
state: "string",
city: {
type: "string",
stream: function(setStream) {
return this.stream(".state").map(function(){
return null;
}).merge(setStream);
}
}
});
var locator = new Locator({
state: "IL",
city: "Chicago"
});
locator.on("city", function(){});
locator.state = "CA";
locator.city //-> null;
Notice, in the can-define-stream
example, city
must be bound for it to work.
Sealed instances and strict mode
By default, DefineMap
instances are sealed. This
means that setting properties that are not defined when the constructor is defined
will throw an error in files that are in strict mode. For example:
"use strict";
var DefineMap = require("can-define/map/map");
var MyType = DefineMap.extend({
myProp: "string"
});
var myType = new MyType();
myType.myProp = "value"; // no error thrown
myType.otherProp = "value" // throws Error!
Read the seal documentation for more information on this behavior.