UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

325 lines (286 loc) 9.67 kB
steal('can/util', './bubble.js','../map_helpers.js', 'can/map', 'can/list', './nested_reference.js', function (can, bubble, mapHelpers) { can.LazyMap = can.Map.extend({ _bubble: bubble }, { setup: function (obj) { this.constructor.Map = this.constructor; this.constructor.List = can.LazyList; // `_data` is where we keep the properties. this._data = can.extend(can.extend(true, {}, this._setupDefaults() || {}), obj); // The namespace this `object` uses to listen to events. can.cid(this, ".lazyMap"); // Sets all `attrs`. this._setupComputedProperties(); var teardownMapping = obj && mapHelpers.addToMap(obj, this); // keep references to Observes in `_data` this._nestedReference = new can.NestedReference(this._data); if (teardownMapping) { teardownMapping(); } // Make the data directly accessible (if possible) can.each(this._data, can.proxy(function(value, prop) { this.___set(prop, value); }, this)); this.bind('change', can.proxy(this._changes, this)); }, _changes: function (ev, attr, how, newVal, oldVal) { }, // todo: function should be renamed _addChild: function (path, newChild, setNewChild) { var self = this; // remove 'old' references that are starting with `path` and do rewiring this._nestedReference.removeChildren(path, function (oldChild, oldChildPath) { // unhook every current child on path bubble.remove(self, oldChild); // if `newChild` passed bind it to every child and make references (1st step: rewiring to bottom/children) if (newChild) { var newChildPath = oldChildPath.replace(path + ".", ""); // check if we are replacing existing observe or inserting new one if (path === newChildPath) { // oldChild._nestedReference.each(function (obj, path) { newChild._nestedReference.make(path()); if (self._bindings) { bubble.add(this, newChild, path()); } }); // } else { var reference = newChild._nestedReference.make(newChildPath); if (self._bindings) { bubble.add(oldChild, newChild, reference()); } } } }); // callback if (setNewChild) { setNewChild(); } // bind parent on `newChild` and make reference (2st step: rewiring to top/parent) if (newChild) { var reference = this._nestedReference.make(path); if (this._bindings) { bubble.add(this, newChild, reference()); } } return newChild; }, removeAttr: function (attr) { var data = this._goto(attr); // if there are more attr parts remaining, it means we // hit an internal observable if (data.parts.length) { // ask that observable to remove the attr return data.value.removeAttr(data.parts.join(".")); } else { // otherwise, are we removing a property from an array if (can.isArray(data.parent)) { data.parent.splice(data.prop, 1); this._triggerChange(attr, "remove", undefined, [this.__type(data.value, data.prop)]); } else { // do not trigger if prop does not exists if (data.parent[data.prop]) { delete data.parent[data.prop]; can.batch.trigger(this, data.path.length ? data.path.join(".") + ".__keys" : "__keys"); this._triggerChange(attr, "remove", undefined, this.__type(data.value, data.prop)); } } // unhookup anything that was in here //this._addChild(attr); // --> CHECK THIS ONE! (previous bug was causing this to work even if it shouldn't,) // instead remove all references, do not unbind as _addChild does this._nestedReference.removeChildren(); return data.value; } }, // converts the value into an observable if needed __type: function(value, prop){ // If we are getting an object. if (!( value instanceof can.LazyMap) && mapHelpers.canMakeObserve(value) ) { if( can.isArray(value) ) { var List = can.LazyList; return new List(value); } else { var Map = this.constructor.Map || can.LazyMap; return new Map(value); } } return value; }, // walks to a property on the lazy map // if it finds an object, uses [] to follow properties // if it finds something else, it uses __get _goto: function (attr, keepKey) { var parts = mapHelpers.attrParts(attr, keepKey).slice(0), prev, path = [], part; // are we dealing with list or map var cur = this instanceof can.List ? this[parts.shift()] : this.___get(); // TODO we might also have to check for dot separated keys in each iteration while (cur && !can.isMapLike(cur) && parts.length) { if (part !== undefined) { path.push(part); } prev = cur; cur = cur[part = parts.shift()]; } return { parts: parts, prop: part, value: cur, parent: prev, path: path }; }, // Reads a property from the `object`. _get: function (attr) { can.__observe(this, attr); var data = this._goto(attr); // if it's already observe return it if (can.isMapLike(data.value)) { if (data.parts.length) { return data.value._get(data.parts); } else { return data.value; } } else if (data.value && mapHelpers.canMakeObserve(data.value)) { // if object create LazyMap/LazyList var converted = this.__type(data.value, data.prop); // ... and replace it this._addChild(attr, converted, function () { data.parent[data.prop] = converted; }); return converted; } else if(data.value !== undefined) { // Return if we have a value return data.value; } else { // Otherwise get it directly from this object return this.__get(attr); } }, // Sets `attr` prop as value on this object where. // `attr` - Is a string of properties or an array of property values. // `value` - The raw value to set. _set: function (attr, value, keepKey) { var data = this._goto(attr, keepKey); if (can.isMapLike(data.value) && data.parts.length) { return data.value._set(data.parts, value); } else if (!data.parts.length) { this.__set(attr, value, data.value, data); } else { throw new Error("can.LazyMap: object does not exist"); } }, __set: function (prop, value, current, data, convert) { // Otherwise, we are setting it on this `object`. // are we changing the value. // maybe not needed at all convert = convert || true; if (value !== current) { // Check if we are adding this for the first time -- // if we are, we need to create an `add` event. var changeType = data.parent.hasOwnProperty(data.prop) ? "set" : "add"; // if it is or should be a Lazy if (convert && mapHelpers.canMakeObserve(value)) { // make it a lazy value = this.__type(value, prop); var self = this; // hook up it's bindings this._addChild(prop, value, function () { // set the value self.___set(prop, value, data); }); } else { // just set the value this.___set(prop, value, data); } if (changeType === "add") { // If there is no current value, let others know that // the the number of keys have changed can.batch.trigger(this, data.path.length ? data.path.join(".") + ".__keys" : "__keys", undefined); } // `batchTrigger` the change event. this._triggerChange(prop, changeType, value, current); } }, // Directly sets a property on this `object`. ___set: function (prop, val, data) { var computedAttr = this._computedAttrs[prop]; if (computedAttr) { computedAttr.compute(val); } else if (data) { data.parent[data.prop] = val; } else { this._data[prop] = val; } // Add property directly for easy writing. // Check if its on the `prototype` so we don't overwrite methods like `attrs`. if (!(can.isFunction(this.constructor.prototype[prop]))) { this[prop] = val; } }, /** * @hide * Set multiple properties on the observable * @param {Object} props * @param {Boolean} remove true if you should remove properties that are not in props */ _getAttrs: function(){ return mapHelpers.serialize(this, 'attr', {}); }, _setAttrs: function (props, remove) { props = can.extend({}, props); var self = this, prop, data, newVal; can.batch.start(); // Update existing props this.each(function (curVal, prop) { newVal = props[prop]; data = self._goto(prop, true); // remove existing prop and return if there is no new prop to merge and `remove` param exists if (newVal === undefined) { if (remove) { self.removeAttr(prop); } return; } else if (!can.isMapLike(curVal) && mapHelpers.canMakeObserve(curVal)) { // convert curVal to observe curVal = self.attr(prop); } // if we're dealing with models, want to call _set to let converter run if (newVal instanceof can.Map) { self.__set(prop, newVal, curVal, data); // if its an object, let attr merge } else if (can.isMapLike(curVal) && mapHelpers.canMakeObserve(newVal) && curVal.attr) { curVal.attr(newVal, remove); // otherwise just set } else if (curVal !== newVal) { // OK till here self.__set(prop, newVal, curVal, data); } // delete passed prop after setting delete props[prop]; }); // add remaining props for (prop in props) { newVal = props[prop]; this._set(prop, newVal, true); } can.batch.stop(); return this; } }); can.LazyList = can.List.extend({ Map: can.LazyMap }, { setup: function() { can.List.prototype.setup.apply(this, arguments); this._nestedReference = new can.NestedReference(this); } }); return can.LazyMap; });