can-map
Version:
Observable Objects
813 lines (699 loc) • 24.3 kB
JavaScript
"use strict";
/* jshint -W079 */
// # 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](bubble.html) - Handles bubbling of child events to parent events.
// [map_helpers.js](map_helpers.html) - Assorted helpers for handling cycles during serialization or
// instantition of objects.
var bubble = require('./bubble');
var mapHelpers = require('./map-helpers');
var canEvent = require('can-event-queue/map/map');
var addTypeEvents = require("can-event-queue/type/type");
var Construct = require('can-construct');
var ObservationRecorder = require('can-observation-recorder');
var ObserveReader = require('can-stache-key');
var canCompute = require('can-compute');
var singleReference = require('can-single-reference');
var Observation = require('can-observation');
var namespace = require("can-namespace");
var dev = require("can-log/dev/dev");
var CID = require("can-cid");
var assign = require("can-assign");
var types = require("can-types");
var canReflect = require("can-reflect");
var canSymbol = require("can-symbol");
var CIDSet = require('can-cid/set/set');
var CIDMap = require("can-cid/map/map");
var canQueues = require("can-queues");
// properties that can't be observed on ... no matter what
var unobservable = {
"constructor": true
};
var hasOwnProperty = ({}).hasOwnProperty;
var inSetupSymbol = canSymbol.for("can.initializing");
// Extend [can.Construct](../construct/construct.html) to make inheriting a `can.Map` easier.
var Map = 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) {
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 (Map) {
addTypeEvents(this);
this[canSymbol.for("can.defineInstanceKey")] = function(prop, definition){
if(definition.value !== undefined) {
this.defaults[prop] = definition.value;
}
if(definition.enumerable === false ) {
this.enumerable[prop] = false;
}
};
// Provide warnings if can.Map is used incorrectly.
//!steal-remove-start
if (process.env.NODE_ENV !== 'production') {
if(this.prototype.define && !mapHelpers.define) {
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) {
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 = {};
}
if(!this.enumerable) {
this.enumerable = {};
}
// 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 Construct)
) {
this.defaults[prop] = this.prototype[prop];
} else if (canReflect.isObservableLike(this.prototype[prop])) {
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.
// This is something List should weave in.
/*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.
addEventListener: canEvent.addEventListener,
removeEventListener: canEvent.removeEventListener,
dispatch: canEvent.dispatch,
// ### keys
// An observable way to get the keys from a map.
keys: function (map) {
return canReflect.getOwnEnumerableKeys(map);
}
},
/**
* @prototype
*/
// ## Prototype Properties and Methods
{
// ### setup
// Initializes the map instance's behavior.
setup: function (obj) {
if(canReflect.isObservableLike(obj) && typeof obj.serialize === "function"){
obj = obj.serialize();
}
// Where we keep the values of the compute.
this._data = Object.create(null);
// The namespace this `object` uses to listen to events.
CID(this, ".map");
this._setupComputedProperties();
var teardownMapping = obj && mapHelpers.addToMap(obj, this);
var defaultValues = this._setupDefaults(obj);
var data = assign(canReflect.assignDeep({}, 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 = Object.create(null);
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]);
}
},
// ### _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) {
attr = 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) {
ObservationRecorder.add(this, attr);
return value;
}
var first = attr.substr(0, dotIndex),
second = attr.substr(dotIndex+1);
var current = this.__get( first );
return current && canReflect.getKeyValue(current, second);
} 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]) {
ObservationRecorder.add(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) {
// return computedAttr.compute();
return canReflect.getValue(computedAttr.compute);
} else {
return hasOwnProperty.call(this._data, 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) {
attr = attr + "";
var dotIndex = attr.indexOf('.'),
current;
//!steal-remove-start
if(process.env.NODE_ENV !== 'production') {
var lastItem, lastFn;
// If there are observations currently recording, this isn't a good time to
// mutate values: it's likely a cycle, and even if it doesn't cycle infinitely,
// it will likely cause unnecessary recomputation of derived values. Warn the user.
if(ObservationRecorder.isRecording() && canQueues.stack().length && !this[inSetupSymbol]) {
lastItem = canQueues.stack()[canQueues.stack().length - 1];
lastFn = lastItem.context instanceof Observation ? lastItem.context.func : lastItem.fn;
var mutationWarning = "can-map: The " + attr + " property on " +
canReflect.getName(this) +
" is being set in " +
(canReflect.getName(lastFn) || canReflect.getName(lastItem.fn)) +
". This can cause infinite loops and performance issues. " +
"Use getters and listeners to derive properties instead. https://canjs.com/doc/guides/logic.html#Derivedproperties";
dev.warn(mutationWarning);
canQueues.logStack();
}
}
//!steal-remove-end
if(dotIndex >= 0 && !keepKey){
var first = attr.substr(0, dotIndex),
second = attr.substr(dotIndex+1);
current = this[inSetupSymbol] ? undefined : this.___get( first );
if( canReflect.isMapLike(current) ) {
canReflect.setKeyValue(current, second, value);
} else {
current = this[inSetupSymbol] ? 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);
}
} else {
current = this[inSetupSymbol] ? 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" &&
!canReflect.isObservableLike( value ) &&
mapHelpers.canMakeObserve(value) &&
!canReflect.isListLike(value)
) {
var cached = mapHelpers.getMapFromObject(value);
if(cached) {
return cached;
}
var MapConstructor = this.constructor.Map || Map;
return new MapConstructor(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 || !Object.prototype.hasOwnProperty.call( this._data, prop ) ) {
var computedAttr = this._computedAttrs[prop];
// Dispatch an "add" event if adding a new property.
var changeType = computedAttr || current !== undefined ||
hasOwnProperty.call(this.___get(), 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 ) {
canReflect.setValue(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 canReflect.deleteKeyValue(current, parts.join("."));
} 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){
if(this._legacyAttrBehavior) {
return mapHelpers.getValue(this, name, val, "serialize");
} else {
return canReflect.serialize(val, CIDMap);
}
},
// ### _getAttrs
// Returns the values of all attributes as a plain JavaScript object.
_getAttrs: function(){
if(this._legacyAttrBehavior) {
return mapHelpers.serialize(this, 'attr', canReflect.isListLike(this) ? [] : {});
} else {
return canReflect.unwrap(this, CIDMap);
}
},
// ### _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) {
if(this._legacyAttrBehavior) {
return this.__setAttrs(props, remove);
}
if(remove === true || remove === "true") {
this[canSymbol.for("can.updateDeep")](props);
} else {
this[canSymbol.for("can.assignDeep")](props);
}
return this;
},
__setAttrs: function (props, remove) {
props = assign({}, props);
var prop,
self = this,
newVal;
// Batch all of the change events until we are done.
canQueues.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 ( canReflect.isObservableLike(curVal) && canReflect.isMapLike(curVal) && mapHelpers.canMakeObserve(newVal) ) {
if(remove === true) {
canReflect.updateDeep(curVal, newVal);
} else {
canReflect.assignDeep(curVal, newVal);
}
// 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);
}
}
canQueues.batch.stop();
return this;
},
serialize: function () {
return canReflect.serialize(this, CIDMap);
},
// ### _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) {
canQueues.batch.start();
if(bubble.isBubbling(this, "change")) {
canEvent.dispatch.call(this, {
type: "change",
target: this,
batchNum: batchNum
}, [attr, how, newVal, oldVal]);
}
canEvent.dispatch.call(this, {
type: attr,
target: this,
batchNum: batchNum,
patches: [{type: "set", key: attr, value: newVal}]
}, [newVal, oldVal]);
if(how === "remove" || how === "add") {
canEvent.dispatch.call(this, {
type: "__keys",
target: this,
batchNum: batchNum
});
}
canQueues.batch.stop();
},
// ### 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 (typeof this.constructor.prototype[prop] === "function") {
return canCompute(this[prop], this);
} else {
var reads = ObserveReader.reads(prop);
var last = reads.length - 1;
return canCompute(function (newVal) {
if (arguments.length) {
ObserveReader.write(this, reads[last].key, newVal, {});
} else {
return ObserveReader.get(this, prop);
}
}, this);
}
},
// ### each
// loops through all the key-value pairs on this map.
forEach: function (callback, context) {
var key, item;
var keys = canReflect.getOwnEnumerableKeys(this);
for(var i =0, len = keys.length; i < len; i++) {
key = keys[i];
item = this.attr(key);
if (callback.call(context || item, item, key, this) === false) {
break;
}
}
return this;
},
// ### _each
// Iterator that does not trigger live binding.
_each: function (callback) {
var data = this.___get();
for (var prop in data) {
if (hasOwnProperty.call(data, prop)) {
callback(data[prop], prop);
}
}
},
dispatch: canEvent.dispatch
});
// makes it so things can read this.
canEvent(Map.prototype);
// ### 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.
Map.prototype.addEventListener = function (eventName, handler) {
var computedBinding = this._computedAttrs && this._computedAttrs[eventName];
if (computedBinding && computedBinding.compute) {
if (!computedBinding.count) {
computedBinding.count = 1;
canReflect.onValue(computedBinding.compute, computedBinding.handler, "notify");
} else {
computedBinding.count++;
}
}
// Sets up bubbling if needed.
bubble.bind(this, eventName);
return canEvent.addEventListener.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.
Map.prototype.removeEventListener = function (eventName, handler) {
var computedBinding = this._computedAttrs && this._computedAttrs[eventName];
if (computedBinding) {
if (computedBinding.count === 1) {
computedBinding.count = 0;
canReflect.offValue(computedBinding.compute, computedBinding.handler, "notify");
} else {
computedBinding.count--;
}
}
// Teardown bubbling if needed.
bubble.unbind(this, eventName);
return canEvent.removeEventListener.apply(this, arguments);
};
// ### etc
// Setup on/off aliases
Map.prototype.on = Map.prototype.bind = Map.prototype.addEventListener;
Map.prototype.off = Map.prototype.unbind = Map.prototype.removeEventListener;
Map.on = Map.bind = Map.addEventListener;
Map.off = Map.unbind = Map.removeEventListener;
// - type -
canReflect.assignSymbols(Map.prototype,{
// -type-
"can.isMapLike": true,
"can.isListLike": false,
"can.isValueLike": false,
// -get/set-
"can.getKeyValue": Map.prototype._get,
"can.setKeyValue": Map.prototype._set,
"can.deleteKeyValue": Map.prototype._remove,
// -shape
"can.getOwnEnumerableKeys": function(){
if (!this[inSetupSymbol]) {
ObservationRecorder.add(this, '__keys');
}
var enumerable = this.constructor.enumerable;
if(enumerable) {
return Object.keys(this._data).filter(function(key){
return enumerable[key] !== false;
},this);
} else {
return Object.keys(this._data);
}
},
// -shape get/set-
"can.assignDeep": function(source){
canQueues.batch.start();
// TODO: we should probably just throw an error instead of cleaning
canReflect.assignDeepMap(this, mapHelpers.removeSpecialKeys(canReflect.assignMap({}, source)));
canQueues.batch.stop();
},
"can.updateDeep": function(source){
canQueues.batch.start();
// TODO: we should probably just throw an error instead of cleaning
canReflect.updateDeepMap(this, mapHelpers.removeSpecialKeys(canReflect.assignMap({}, source)));
canQueues.batch.stop();
},
"can.unwrap": mapHelpers.reflectUnwrap,
"can.serialize": mapHelpers.reflectSerialize,
// observable
"can.onKeyValue": function(key, handler, queue){
var translationHandler = function(ev, newValue, oldValue){
handler.call(this, newValue, oldValue);
};
singleReference.set(handler, this, translationHandler, key);
this.addEventListener(key, translationHandler, queue);
},
"can.offKeyValue": function(key, handler, queue){
this.removeEventListener(key, singleReference.getAndDelete(handler, this, key), queue );
},
"can.keyHasDependencies": function(key) {
return !!(this._computedAttrs && this._computedAttrs[key] &&
this._computedAttrs[key].compute);
},
"can.getKeyDependencies": function(key) {
var ret;
if(this._computedAttrs && this._computedAttrs[key] && this._computedAttrs[key].compute) {
ret = {};
ret.valueDependencies = new CIDSet();
ret.valueDependencies.add(this._computedAttrs[key].compute);
}
return ret;
}
});
if(!types.DefaultMap) {
types.DefaultMap = Map;
}
module.exports = namespace.Map = Map;