• Jump To … +
    bubble.js map.js map_benchmark.js map_helpers.js
  • ¶

    can/map/map.js (aka can.Map)

    can.Map provides the observable pattern for JavaScript objects. It provides an attr and removeAttr method that can be used to get/set and remove properties and nested properties by calling a “pipeline” of protected methods:

    • _get, _set, _remove - handle nested properties.
    • __get, __set, __remove - handle triggering events.
    • ___get, ___set, ___remove - read / write / remove raw values.

    When attr gets or sets multiple properties it calls _getAttrs or _setAttrs.

    bubble.js - Handles bubbling of child events to parent events. map_helpers.js - Assorted helpers for handling cycles during serialization or instantition of objects.

    steal('can/util', 'can/util/bind','./bubble.js', './map_helpers.js','can/construct', 'can/util/batch', 'can/compute/get_value_and_bind.js', function (can, bind, bubble, mapHelpers) {
  • ¶

    properties that can’t be observed on … no matter what

    	var unobservable = {
    		"constructor": true
    	};
  • ¶

    Extend can.Construct to make inheriting a can.Map easier.

    	var Map = can.Map = can.Construct.extend(
    		/**
    		 * @static
    		 */
  • ¶

    Static Properties and Methods

    		{
  • ¶

    setup

    Called when a Map constructor is defined/extended to perform any initialization behavior for the new constructor function.

    			setup: function (baseMap) {
    
    				can.Construct.setup.apply(this, arguments);
  • ¶

    A cached list of computed properties on the prototype.

    				this._computedPropertyNames = [];
  • ¶

    Do not run if we are defining can.Map.

    				if (can.Map) {
  • ¶

    Provide warnings if can.Map is used incorrectly. !steal-remove-start

    					if(this.prototype.define && !mapHelpers.define) {
    						can.dev.warn("can/map/define is not included, yet there is a define property "+
    						"used. You may want to add this plugin.");
    					}
    					if(this.define && !mapHelpers.define) {
    						can.dev.warn("The define property should be on the map's prototype properties, "+
    						"not the static properties. Also, can/map/define is not included.");
    					}
  • ¶

    !steal-remove-end

  • ¶

    Create a placeholder for default values.

    					if (!this.defaults) {
    						this.defaults = {};
    					}
  • ¶

    Go through everything on the prototype. If it’s a primitive, treat it as a default value. If it’s a compute, identify it so it can be setup as a computed property.

    					for (var prop in this.prototype) {
    						if (
    							prop !== "define" &&
    							prop !== "constructor" &&
    							(
    							typeof this.prototype[prop] !== "function" ||
    							this.prototype[prop].prototype instanceof can.Construct
    							)
    						) {
    							this.defaults[prop] = this.prototype[prop];
    						} else if (this.prototype[prop].isComputed) {
    							this._computedPropertyNames.push(prop);
    						}
    					}
  • ¶

    If define is a function, call it with this can.Map

    					if(mapHelpers.define) {
    						mapHelpers.define(this, baseMap.prototype.define);
    					}
    				}
  • ¶

    If we inherit from can.Map, but not can.List, create a can.List that creates instances of this Map type.

    				if (can.List && !(this.prototype instanceof can.List)) {
    					this.List = Map.List.extend({
    						Map: this
    					}, {});
    				}
    
    			},
  • ¶

    shortName

    Tells can.Construct to show instance as Map in the debugger.

    			shortName: "Map",
  • ¶

    _bubbleRule

    Returns which events to setup bubbling on for a given bound event. By default, only bubbles “change” events if someone listens to a “change” event or a nested event like “foo.bar”.

    			_bubbleRule: function(eventName) {
    				return (eventName === "change" || eventName.indexOf(".") >= 0 ) ?
    					["change"] :
    					[];
    			},
  • ¶

    bind, unbind

    Listen to events on the Map constructor. These are here mostly for can.Model.

    			bind: can.bindAndSetup,
    			unbind: can.unbindAndTeardown,
  • ¶

    id

    Name of the id field. Used in can.Model.

    			id: "id",
  • ¶

    keys

    An observable way to get the keys from a map.

    			keys: function (map) {
    				var keys = [];
    				can.__observe(map, '__keys');
    				for (var keyName in map._data) {
    					keys.push(keyName);
    				}
    				return keys;
    			}
    		},
    		/**
    		 * @prototype
    		 */
  • ¶

    Prototype Properties and Methods

    		{
  • ¶

    setup

    Initializes the map instance’s behavior.

    			setup: function (obj) {
    
    				if(obj instanceof can.Map){
    					obj = obj.serialize();
    				}
  • ¶

    Where we keep the values of the compute.

    				this._data = {};
  • ¶

    The namespace this object uses to listen to events.

    				can.cid(this, ".map");
    
    				this._setupComputedProperties();
    
    				var teardownMapping = obj && mapHelpers.addToMap(obj, this);
    
    				var defaultValues = this._setupDefaults(obj);
    				var data = can.extend(can.extend(true, {}, defaultValues), obj);
    
    				this.attr(data);
    
    				if (teardownMapping) {
    					teardownMapping();
    				}
    			},
  • ¶

    _setupComputes

    Sets up computed properties on a Map. Stores information for each computed property on this._computedAttrs that looks like:

    {
      // the number of bindings on this property
      count: 1,
      // a handler that forwards events on the compute
      // to the map instance
      handler: handler,
      compute: compute  // the compute
    }
    
    			_setupComputedProperties: function () {
    				this._computedAttrs = {};
    
    				var computes = this.constructor._computedPropertyNames;
    
    				for (var i = 0, len = computes.length; i < len; i++) {
    					var attrName = computes[i];
    					mapHelpers.addComputedAttr(this, attrName, this[attrName].clone(this));
    				}
    			},
  • ¶

    _setupDefaults

    Returns the default values for the instance.

    			_setupDefaults: function(){
    				return this.constructor.defaults || {};
    			},
  • ¶

    attr

    The primary get/set interface for can.Map. Calls _get, _set or _attrs depending on how it is called.

    			attr: function (attr, val) {
    				var type = typeof attr;
    				if(attr === undefined) {
    					return this._getAttrs();
    				} else if (type !== "string" && type !== "number") {
  • ¶

    Get or set multiple attributes.

    					return this._setAttrs(attr, val);
    				}
    				else if (arguments.length === 1) {
  • ¶

    Get a single attribute.

    					return this._get(attr+"");
    				} else {
  • ¶

    Set an attribute.

    					this._set(attr+"", val);
    					return this;
    				}
    			},
  • ¶

    _get

    Handles reading nested properties like “foo.bar” by getting the value of “foo” and recursively calling _get for the value of “bar”. To read the actual values, _get calls ___get.

    			_get: function (attr) {
    				var dotIndex = attr.indexOf('.');
    
    				if( dotIndex >= 0 ) {
  • ¶

    Attempt to get the value anyway in case somone wrote new can.Map({"foo.bar": 1}).

    					var value = this.___get(attr);
    					if (value !== undefined) {
    						can.__observe(this, attr);
    						return value;
    					}
    
    					var first = attr.substr(0, dotIndex),
    						second = attr.substr(dotIndex+1);
    
    					var current = this.__get( first );
    
    					return current && current._get ?  current._get(second) : undefined;
    				} else {
    					return this.__get( attr );
    				}
    			},
  • ¶

    __get

    Signals can.compute that an observable property is being read.

    			__get: function(attr){
    				if(!unobservable[attr] && !this._computedAttrs[attr]) {
    					can.__observe(this, attr);
    				}
    				return this.___get( attr );
    			},
  • ¶

    ___get

    When called with an argument, returns the value of this property. If that property is represented by a computed attribute, return the value of that compute. If no argument is provided, return the raw data.

    			___get: function (attr) {
    				if (attr !== undefined) {
    					var computedAttr = this._computedAttrs[attr];
    					if (computedAttr && computedAttr.compute) {
  • ¶

    return computedAttr.compute();

    						return computedAttr.compute();
    					} else {
    						return this._data.hasOwnProperty(attr) ? this._data[attr] : undefined;
    					}
    				} else {
    					return this._data;
    				}
    			},
  • ¶

    _set

    Handles setting nested properties by finding the nested observable and recursively calling _set on it. Eventually, it calls __set with the __type converted value to set and the current value. The current value is passed for two reasons:

    • so __set can trigger an event if the value has changed.
    • for advanced setting behavior that define.set can do.

    If the map is initializing, the current value does not need to be read because no change events are dispatched anyway.

    			_set: function (attr, value, keepKey) {
    
    				var dotIndex = attr.indexOf('.'),
    					current;
    
    				if(dotIndex >= 0 && !keepKey){
    					var first = attr.substr(0, dotIndex),
    						second = attr.substr(dotIndex+1);
    
    					current =  this.__inSetup ? undefined : this.___get( first );
    
    					if( can.isMapLike(current) ) {
    						current._set(second, value);
    					} else {
    						throw new Error("can.Map: Object does not exist");
    					}
    
    				} else {
    					current = this.__inSetup ? undefined : this.___get( attr );
  • ¶

    //Convert if there is a converter. Remove in 3.0.

    					if (this.__convert) {
    						value = this.__convert(attr, value);
    					}
    
    					this.__set(attr, this.__type(value, attr), current);
    				}
    			},
  • ¶

    __type

    Converts set values to another type. By default, this converts Objects to can.Maps and Arrays to can.Lists. This also makes it so if a plain JavaScript object has already been converted to a list or map, that same list or map instance is used.

    			__type: function(value, prop){
    
    				if (typeof value === "object" && !( value instanceof can.Map) && mapHelpers.canMakeObserve(value)  ) {
    
    					var cached = mapHelpers.getMapFromObject(value);
    					if(cached) {
    						return cached;
    					}
    					if( can.isArray(value) ) {
    						var List = can.List;
    						return new List(value);
    					} else {
    						var Map = this.constructor.Map || can.Map;
    						return new Map(value);
    					}
    				}
    				return value;
    			},
  • ¶

    __set

    Handles firing events if the value has changed and works with the bubble helpers to setup bubbling. Calls ___set to do the actual setting.

    			__set: function (prop, value, current) {
    
    				if (value !== current) {
    					var computedAttr = this._computedAttrs[prop];
  • ¶

    Dispatch an “add” event if adding a new property.

    					var changeType = computedAttr || current !== undefined || this.___get()
    						.hasOwnProperty(prop) ? "set" : "add";
  • ¶

    Set the value on _data and set up bubbling.

    					this.___set(prop, typeof value === "object" ? bubble.set(this, prop, value, current) : value );
  • ¶

    Computed properties change events are already forwarded except if no one is listening to them.

    					if(!computedAttr || !computedAttr.count) {
    						this._triggerChange(prop, changeType, value, current);
    					}
  • ¶

    Stop bubbling old nested maps.

    					if (typeof current === "object") {
    						bubble.teardownFromParent(this, current);
    					}
    				}
    			},
  • ¶

    ___set

    Directly saves the set value as a property on _data or sets the computed attribute.

    			___set: function (prop, val) {
    				var computedAttr = this._computedAttrs[prop];
    				if ( computedAttr && computedAttr.compute ) {
    					computedAttr.compute(val);
    				} else {
    					this._data[prop] = val;
    				}
  • ¶

    Adds the property directly to the map instance. But first, checks that it’s not overwriting a method. This should be removed in 3.0.

    				if ( typeof this.constructor.prototype[prop] !== 'function' && !computedAttr ) {
    					this[prop] = val;
    				}
    			},
    
    			removeAttr: function (attr) {
    				return this._remove(attr);
    			},
  • ¶

    _remove

    Handles removing nested observes.

    			_remove: function(attr){
  • ¶

    If this is List.

    				var parts = mapHelpers.attrParts(attr),
  • ¶

    The actual property to remove.

    					prop = parts.shift(),
  • ¶

    The current value.

    					current = this.___get(prop);
  • ¶

    If we have more parts, call removeAttr on that part.

    				if (parts.length && current) {
    					return current.removeAttr(parts);
    				} else {
  • ¶

    If attr does not have a .

    					if (typeof attr === 'string' && !!~attr.indexOf('.')) {
    						prop = attr;
    					}
    
    					this.__remove(prop, current);
    					return current;
    				}
    			},
  • ¶

    __remove

    Handles triggering an event if a property could be removed.

    			__remove: function(prop, current){
    				if (prop in this._data) {
    					this.___remove(prop);
  • ¶

    Let others now this property has been removed.

    					this._triggerChange(prop, "remove", undefined, current);
    				}
    			},
  • ¶

    ___remove

    Deletes a property from _data and the map instance.

    			___remove: function(prop){
    				delete this._data[prop];
    				if (!(prop in this.constructor.prototype)) {
    					delete this[prop];
    				}
    			},
  • ¶

    ___serialize

    Serializes a property. Uses map helpers to recursively serialize nested observables.

    			___serialize: function(name, val){
    				return mapHelpers.getValue(this, name, val, "serialize");
    			},
  • ¶

    _getAttrs

    Returns the values of all attributes as a plain JavaScript object.

    			_getAttrs: function(){
    				return mapHelpers.serialize(this, 'attr', {});
    			},
  • ¶

    _setAttrs

    Sets multiple properties on this object at once. First, goes through all current properties and either merges or removes old properties. Then it goes through the remaining ones to be added and sets those properties.

    			_setAttrs: function (props, remove) {
    				props = can.simpleExtend({}, props);
    				var prop,
    					self = this,
    					newVal;
  • ¶

    Batch all of the change events until we are done.

    				can.batch.start();
  • ¶

    Merge current properties with the new ones.

    				this._each(function (curVal, prop) {
  • ¶

    You can not have a _cid property; abort.

    					if (prop === "_cid") {
    						return;
    					}
    					newVal = props[prop];
  • ¶

    If we are merging, remove the property if it has no value.

    					if (newVal === undefined) {
    						if (remove) {
    							self.removeAttr(prop);
    						}
    						return;
    					}
  • ¶

    Run converter if there is one. Remove in 3.0.

    					if (self.__convert) {
    						newVal = self.__convert( prop, newVal );
    					}
    
    					if ( can.isMapLike(curVal) && mapHelpers.canMakeObserve(newVal) ) {
    						curVal.attr(newVal, remove);
  • ¶

    Otherwise just set.

    					} else if (curVal !== newVal) {
    						self.__set(prop, self.__type(newVal, prop), curVal);
    					}
    
    					delete props[prop];
    				});
  • ¶

    Add remaining props.

    				for (prop in props) {
  • ¶

    Ignore _cid.

    					if (prop !== "_cid") {
    						newVal = props[prop];
    						this._set(prop, newVal, true);
    					}
    
    				}
    				can.batch.stop();
    				return this;
    			},
    
    			serialize: function () {
    				return mapHelpers.serialize(this, 'serialize', {});
    			},
  • ¶

    _triggerChange

    A helper function used to trigger events on this map. If the map is bubbling, this will fire a change event. Otherwise, it only fires a “named” event. Triggers a “__keys” event if a property has been added or removed.

    			_triggerChange: function (attr, how, newVal, oldVal, batchNum) {
    
    				if(bubble.isBubbling(this, "change")) {
    					can.batch.trigger(this, {
    						type: "change",
    						target: this,
    						batchNum: batchNum
    					}, [attr, how, newVal, oldVal]);
    
    				}
    
    				can.batch.trigger(this, {
    					type: attr,
    					target: this,
    					batchNum: batchNum
    				}, [newVal, oldVal]);
    
    				if(how === "remove" || how === "add") {
    					can.batch.trigger(this, {
    						type: "__keys",
    						target: this,
    						batchNum: batchNum
    					});
    				}
    			},
  • ¶

    _bindsetup and _bindteardown

    Placeholders for bind setup and teardown.

    			_bindsetup: function(){},
    			_bindteardown: function(){},
  • ¶

    one

    Listens once to an event.

    			one: can.one,
  • ¶

    bind

    Listens to an event on a map. If the event is a computed property, listen to the compute and forward its events to this map.

    			bind: function (eventName, handler) {
    
    				var computedBinding = this._computedAttrs && this._computedAttrs[eventName];
    				if (computedBinding && computedBinding.compute) {
    					if (!computedBinding.count) {
    						computedBinding.count = 1;
    						computedBinding.compute.bind("change", computedBinding.handler);
    					} else {
    						computedBinding.count++;
    					}
    
    				}
  • ¶

    Sets up bubbling if needed.

    				bubble.bind(this, eventName);
    
    				return can.bindAndSetup.apply(this, arguments);
    			},
  • ¶

    unbind

    Stops listening to an event. If this is the last listener of a computed property, stop forwarding events of the computed property to this map.

    			unbind: function (eventName, handler) {
    				var computedBinding = this._computedAttrs && this._computedAttrs[eventName];
    				if (computedBinding) {
    					if (computedBinding.count === 1) {
    						computedBinding.count = 0;
    						computedBinding.compute.unbind("change", computedBinding.handler);
    					} else {
    						computedBinding.count--;
    					}
    
    				}
  • ¶

    Teardown bubbling if needed.

    				bubble.unbind(this, eventName);
    				return can.unbindAndTeardown.apply(this, arguments);
    
    			},
  • ¶

    compute

    Creates a compute that represents a value on this map. If the property is a function on the prototype, a “function” compute wil be created. Otherwise, a compute will be created that reads the observable attributes.

    			compute: function (prop) {
    
    				if (can.isFunction(this.constructor.prototype[prop])) {
    
    					return can.compute(this[prop], this);
    				} else {
    
    					var reads = can.compute.read.reads(prop),
    						last = reads.length - 1;
    
    					return can.compute(function (newVal) {
    						if (arguments.length) {
    							can.compute.read(this, reads.slice(0, last))
    								.value.attr(reads[last].key, newVal);
    						} else {
    							return can.compute.read(this, reads, {
    								args: []
    							}).value;
    						}
    					}, this);
    				}
    
    			},
  • ¶

    each

    loops through all the key-value pairs on this map.

    			each: function () {
    				return can.each.apply(undefined, [this].concat(can.makeArray(arguments)));
    			},
  • ¶

    _each

    Iterator that does not trigger live binding.

    			_each: function (callback) {
    				var data = this.___get();
    				for (var prop in data) {
    					if (data.hasOwnProperty(prop)) {
    						callback(data[prop], prop);
    					}
    				}
    			},
    
    			dispatch: can.dispatch
    		});
  • ¶

    etc

    Setup on/off aliases

    	Map.prototype.on = Map.prototype.bind;
    	Map.prototype.off = Map.prototype.unbind;
    	Map.on = Map.bind;
    	Map.off = Map.unbind;
    
    	return Map;
    });