diff --git a/can-define.js b/can-define.js index e1d6037..2b1a219 100644 --- a/can-define.js +++ b/can-define.js @@ -2,7 +2,6 @@ "format cjs"; -var event = require("can-event"); var eventLifecycle = require("can-event/lifecycle/lifecycle"); var canBatch = require("can-event/batch/batch"); var canEvent = require("can-event"); @@ -21,6 +20,9 @@ var each = require("can-util/js/each/each"); var defaults = require("can-util/js/defaults/defaults"); var stringToAny = require("can-util/js/string-to-any/string-to-any"); var ns = require("can-namespace"); +var canSymbol = require("can-symbol"); +var canReflect = require("can-reflect"); +var singleReference = require("can-util/js/single-reference/single-reference"); var eventsProto, define, make, makeDefinition, replaceWith, getDefinitionsAndMethods, @@ -52,6 +54,32 @@ var eachPropertyDescriptor = function(map, cb){ } }; +// #### trapSets +// This private function creates a "value trap" to glue together +// defined getters/setters in can-define with the observable +// patterns in can-reflect that are hooked into elsewhere in +// can-define. The last set value is placed into an instance of the +// value trap on set, so as to make it available as a source value +// for an async getter. +function trapSets(observableValue) { + return { + observable: observableValue, + lastSetValue: undefined, + setValue: function(value) { + // Hold on to this value for next time. + this.lastSetValue = value; + if(this.observable) { + if(canSymbol.for("can.setValue") in this.observable) { + canReflect.setValue(this.observable, value); + } else { + //this.observable.value = this.lastSetValue; + this.observable.update(); + } + } + } + }; +} + module.exports = define = ns.define = function(objPrototype, defines, baseDefine) { // default property definitions on _data @@ -127,12 +155,13 @@ module.exports = define = ns.define = function(objPrototype, defines, baseDefine // Places Symbol.iterator or @@iterator on the prototype // so that this can be iterated with for/of and can-util/js/each/each - if(!objPrototype[types.iterator]) { - defineConfigurableAndNotEnumerable(objPrototype, types.iterator, function(){ + var iteratorSymbol = canSymbol.iterator || canSymbol.for("iterator"); + if(!objPrototype[iteratorSymbol]) { + defineConfigurableAndNotEnumerable(objPrototype, iteratorSymbol, function(){ return new define.Iterator(this); }); } - + return result; }; @@ -267,26 +296,48 @@ make = { return function() { var map = this, defaultValue = defaultValueFn && defaultValueFn.call(this), - computeFn; + computeFn, valueTrap, computeObj; - if (defaultValue) { - computeFn = defaultValue.isComputed ? - defaultValue : - compute.async(defaultValue, get, map); + var boundGet = function() { + return get.call(map, computeObj.valueTrap.lastSetValue); + }; + + if(get.length < 2) { + if(defaultValue && defaultValue.isComputed) { + computeFn = defaultValue; + valueTrap = trapSets(computeFn); + } else { + computeFn = new Observation(boundGet, map); + valueTrap = trapSets(computeFn); + valueTrap.lastSetValue = defaultValue; + } } else { - computeFn = compute.async(defaultValue, get, map); + if (defaultValue) { + computeFn = defaultValue.isComputed ? + defaultValue : + compute.async(defaultValue, get, map); + } else { + computeFn = compute.async(defaultValue, get, map); + } + valueTrap = trapSets(computeFn); } - return { + computeObj = { + oldValue: undefined, compute: computeFn, count: 0, - handler: function(ev, newVal, oldVal) { + handler: function(newVal) { + var oldValue = computeObj.oldValue; + computeObj.oldValue = newVal; canEvent.dispatch.call(map, { type: prop, - target: map - }, [newVal, oldVal]); - } + target: map, + batchNum: canBatch.batchNum + }, [newVal, oldValue]); + }, + valueTrap: valueTrap }; + return computeObj; }; }, // Set related helpers. @@ -298,7 +349,7 @@ make = { }, computed: function(prop) { return function(val) { - this._computed[prop].compute(val); + this._computed[prop].valueTrap.setValue(val); }; }, events: function(prop, getCurrent, setData, eventType) { @@ -475,13 +526,13 @@ make = { computed: function(prop) { // might want to protect this return function() { - return this._computed[prop].compute(); + return canReflect.getValue( this._computed[prop].compute ); }; }, lastSet: function(prop) { return function() { - var lastSetValue = this._computed[prop].compute.computeInstance.lastSetValue; - return lastSetValue && lastSetValue.get(); + var lastSetValue = this._computed[prop].valueTrap.lastSetValue; + return lastSetValue; }; } }, @@ -540,7 +591,7 @@ make = { }, computed: function(prop) { return function() { - return this._computed[prop].compute(); + return canReflect.getValue(this._computed[prop].compute); }; } } @@ -597,7 +648,7 @@ getDefinitionOrMethod = function(prop, value, defaultDefinition){ definition = {type: value}; } else if(typeof value === "function") { - if(types.isConstructor(value)) { + if(canReflect.isConstructorLike(value)) { definition = {Type: value}; } else if(isDefineType(value)) { definition = {type: value}; @@ -681,7 +732,7 @@ replaceWith = function(obj, prop, cb, writable) { }); }; -eventsProto = assign({}, event); +eventsProto = assign({}, canEvent); assign(eventsProto, { _eventSetup: function() {}, _eventTeardown: function() {}, @@ -691,7 +742,8 @@ assign(eventsProto, { if (computedBinding && computedBinding.compute) { if (!computedBinding.count) { computedBinding.count = 1; - computedBinding.compute.addEventListener("change", computedBinding.handler); + canReflect.onValue(computedBinding.compute, computedBinding.handler); + computedBinding.oldValue = canReflect.getValue(computedBinding.compute); } else { computedBinding.count++; } @@ -710,7 +762,7 @@ assign(eventsProto, { if (computedBinding) { if (computedBinding.count === 1) { computedBinding.count = 0; - computedBinding.compute.removeEventListener("change", computedBinding.handler); + canReflect.offValue(computedBinding.compute, computedBinding.handler); } else { computedBinding.count--; } @@ -723,6 +775,18 @@ assign(eventsProto, { }); eventsProto.on = eventsProto.bind = eventsProto.addEventListener; eventsProto.off = eventsProto.unbind = eventsProto.removeEventListener; +canReflect.set(eventsProto, canSymbol.for("can.onKeyValue"), function(key, handler){ + var translationHandler = function(ev, newValue, oldValue){ + handler(newValue, oldValue); + }; + singleReference.set(handler, this, translationHandler, key); + + this.addEventListener(key, translationHandler); +}); + +canReflect.set(eventsProto, canSymbol.for("can.offKeyValue"), function(key, handler){ + this.removeEventListener(key, singleReference.getAndDelete(handler, this, key) ); +}); delete eventsProto.one; diff --git a/define-helpers/define-helpers.js b/define-helpers/define-helpers.js index a0ebd43..9e0b5d6 100644 --- a/define-helpers/define-helpers.js +++ b/define-helpers/define-helpers.js @@ -41,7 +41,12 @@ var defineHelpers = { // next if it's already on this instances var instanceDefines = map._instanceDefinitions; if(!instanceDefines) { - instanceDefines = map._instanceDefinitions = {}; + Object.defineProperty(map, "_instanceDefinitions", { + configurable: true, + enumerable: false, + value: {} + }); + instanceDefines = map._instanceDefinitions; } if(!instanceDefines[prop]) { var defaultDefinition = map._define.defaultDefinition || {type: define.types.observable}; diff --git a/define-test.js b/define-test.js index aafb685..c29e8d4 100644 --- a/define-test.js +++ b/define-test.js @@ -1,12 +1,11 @@ var QUnit = require("steal-qunit"); var compute = require("can-compute"); var define = require("can-define"); -var stache = require("can-stache"); -var CanList = require("can-list"); +var CanList = require("can-define/list/list"); var canBatch = require("can-event/batch/batch"); var isArray = require("can-util/js/is-array/is-array"); var each = require("can-util/js/each/each"); -var types = require("can-types"); +var canSymbol = require("can-symbol"); QUnit.module("can-define"); @@ -921,178 +920,7 @@ test("Asynchronous virtual properties cause extra recomputes (#1915)", function( }); -test("Stache with single property", function() { - var Typer = define.Constructor({ - foo: { - type: 'string' - } - }); - - var template = stache('{{foo}}'); - var t = new Typer({ - foo: 'bar' - }); - var frag = template(t); - equal(frag.firstChild.nodeValue, 'bar'); - t.foo = "baz"; - equal(frag.firstChild.nodeValue, 'baz'); -}); - -test("Stache with boolean property with {{#if}}", function() { - var nailedIt = 'Nailed it'; - var Example = define.Constructor({ - name: { - value: nailedIt - } - }); - - var NestedMap = define.Constructor({ - isEnabled: { - value: true - }, - test: { - Value: Example - }, - examples: { - type: { - one: { - Value: Example - }, - two: { - type: { - deep: { - Value: Example - } - }, - Value: Object - } - }, - Value: Object - } - }); - - var nested = new NestedMap(); - var template = stache('{{#if isEnabled}}Enabled{{/if}}'); - var frag = template(nested); - equal(frag.firstChild.nodeValue, 'Enabled'); -}); - -test("stache with double property", function() { - var nailedIt = 'Nailed it'; - var Example = define.Constructor({ - name: { - value: nailedIt - } - }); - - var NestedMap = define.Constructor({ - isEnabled: { - value: true - }, - test: { - Value: Example - }, - examples: { - type: { - one: { - Value: Example - }, - two: { - type: { - deep: { - Value: Example - } - }, - Value: Object - } - }, - Value: Object - } - }); - - var nested = new NestedMap(); - var template = stache('{{test.name}}'); - var frag = template(nested); - equal(frag.firstChild.nodeValue, nailedIt); -}); - -test("Stache with one nested property", function() { - var nailedIt = 'Nailed it'; - var Example = define.Constructor({ - name: { - value: nailedIt - } - }); - - var NestedMap = define.Constructor({ - isEnabled: { - value: true - }, - test: { - Value: Example - }, - examples: { - type: { - one: { - Value: Example - }, - two: { - type: { - deep: { - Value: Example - } - }, - Value: Object - } - }, - Value: Object - } - }); - var nested = new NestedMap(); - var template = stache('{{examples.one.name}}'); - var frag = template(nested); - equal(frag.firstChild.nodeValue, nailedIt); -}); - -test("Stache with two nested property", function() { - var nailedIt = 'Nailed it'; - var Example = define.Constructor({ - name: { - value: nailedIt - } - }); - - var NestedMap = define.Constructor({ - isEnabled: { - value: true - }, - test: { - Value: Example - }, - examples: { - type: { - one: { - Value: Example - }, - two: { - type: { - deep: { - Value: Example - } - }, - Value: Object - } - }, - Value: Object - } - }); - - var nested = new NestedMap(); - var template = stache('{{examples.two.deep.name}}'); - var frag = template(nested); - equal(frag.firstChild.nodeValue, nailedIt); -}); QUnit.test('Default values cannot be set (#8)', function() { var Person = function() {}; @@ -1315,10 +1143,10 @@ QUnit.test("Properties are enumerable", function() { }); }); -QUnit.test("Doesn't override types.iterator if already on the prototype", function() { +QUnit.test("Doesn't override canSymbol.iterator if already on the prototype", function() { function MyMap() {} - MyMap.prototype[types.iterator] = function() { + MyMap.prototype[canSymbol.iterator || canSymbol.for("iterator")] = function() { var i = 0; return { next: function() { diff --git a/list/list-test.js b/list/list-test.js index 6203378..f37df42 100644 --- a/list/list-test.js +++ b/list/list-test.js @@ -4,12 +4,12 @@ var DefineList = require("can-define/list/list"); var DefineMap = require("can-define/map/map"); var Observation = require("can-observation"); var define = require("can-define"); +var compute = require("can-compute"); +var canReflect = require("can-reflect"); +var canSymbol = require("can-symbol"); var assign = require("can-util/js/assign/assign"); var CID = require("can-cid"); -var types = require("can-types"); -var stache = require("can-stache"); - QUnit.module("can-define/list/list"); QUnit.test("List is an event emitter", function(assert) { @@ -24,10 +24,10 @@ QUnit.test("creating an instance", function() { list.on("add", function(ev, newVals, index) { QUnit.deepEqual(newVals, [ "d" ]); - QUnit.equal(index, 3); - }); + QUnit.equal(index, 3); + }); - list.push("d"); + list.push("d"); }); test('list attr changes length', function() { @@ -41,7 +41,7 @@ test('list attr changes length', function() { }); test('remove on pop', function() { var l = new DefineList([ 0, 1, 2 ]); - l.pop(); + l.pop(); equal(l.length, 2); deepEqual(l.get(), [ 0, 1 ]); @@ -55,20 +55,20 @@ test('list splice', function() { ]); l.on('add', function(ev, newVals, index) { - deepEqual(newVals, [ - 'a', - 'b' - ], 'got the right newVals'); - equal(index, 1, 'adding items'); - }); + deepEqual(newVals, [ + 'a', + 'b' + ], 'got the right newVals'); + equal(index, 1, 'adding items'); + }); l.on('remove', function(ev, oldVals, index) { - deepEqual(oldVals, [ - 1, - 2 - ], 'got the right oldVals'); - equal(index, 1, 'no new Vals'); - }); + deepEqual(oldVals, [ + 1, + 2 + ], 'got the right oldVals'); + equal(index, 1, 'no new Vals'); + }); l.splice(1, 2, 'a', 'b'); deepEqual(l.get(), [ @@ -82,10 +82,10 @@ test('list splice', function() { test('Array accessor methods', 11, function() { var l = new DefineList([ - 'a', - 'b', - 'c' - ]), + 'a', + 'b', + 'c' + ]), sliced = l.slice(2), joined = l.join(' | '), concatenated = l.concat([ @@ -207,7 +207,7 @@ test('No Add Events if DefineList Splice adds the same items that it is removing ok(false, 'Remove callback should not be called.'); }); - var result = list.splice(0, 2, "a", "b"); + var result = list.splice(0, 2, "a", "b"); deepEqual(result, [ "a", "b" ]); }); @@ -232,7 +232,7 @@ test("Setting with .set() out of bounds of length triggers add event with leadin var list = new DefineList([ 1 ]); list.bind("add", function(ev, newElements, index) { deepEqual(newElements, [ undefined, undefined, 4 ], - "Leading undefineds are included"); + "Leading undefineds are included"); equal(index, 1, "Index takes into account the leading undefineds from a .set()"); }); list.set(3, 4); @@ -317,28 +317,28 @@ test("slice and join are observable by a compute (#1884)", function() { }); test('list.replace', function() { - var firstArray = [ + var firstArray = [ { id: 1, name: "Marshall" }, { id: 2, name: "Austin" }, { id: 3, name: "Hyrum" } - ]; - var myList = new DefineList(firstArray); - var newArray = [ + ]; + var myList = new DefineList(firstArray); + var newArray = [ { id: 4, name: "Aubree" }, { id: 5, name: "Leah" }, { id: 6, name: 'Lily' } - ]; - myList.replace(newArray); - equal(myList.length, 3); - equal(myList[0].name, "Aubree"); - equal(myList[1].name, "Leah"); - equal(myList[2].name, "Lily", "Can replace a List with an Array."); - - myList.replace(firstArray); - equal(myList.length, 3); - equal(myList[0].name, "Marshall"); - equal(myList[1].name, "Austin"); - equal(myList[2].name, "Hyrum", "Can replace a List with another List."); + ]; + myList.replace(newArray); + equal(myList.length, 3); + equal(myList[0].name, "Aubree"); + equal(myList[1].name, "Leah"); + equal(myList[2].name, "Lily", "Can replace a List with an Array."); + + myList.replace(firstArray); + equal(myList.length, 3); + equal(myList[0].name, "Marshall"); + equal(myList[1].name, "Austin"); + equal(myList[2].name, "Hyrum", "Can replace a List with another List."); }); test('list.map', function() { @@ -346,30 +346,30 @@ test('list.map', function() { { id: 1, name: "Marshall" }, { id: 2, name: "Austin" }, { id: 3, name: "Hyrum" } - ]; - var myList = new DefineList(myArray); - var newList = myList.map(function(person) { - person.lastName = "Thompson"; - return person; - }); - - equal(newList.length, 3); - equal(newList[0].name, "Marshall"); - equal(newList[0].lastName, "Thompson"); - equal(newList[1].name, "Austin"); - equal(newList[1].lastName, "Thompson"); - equal(newList[2].name, "Hyrum"); - equal(newList[2].lastName, "Thompson"); - - var ExtendedList = DefineList.extend({ + ]; + var myList = new DefineList(myArray); + var newList = myList.map(function(person) { + person.lastName = "Thompson"; + return person; + }); + + equal(newList.length, 3); + equal(newList[0].name, "Marshall"); + equal(newList[0].lastName, "Thompson"); + equal(newList[1].name, "Austin"); + equal(newList[1].lastName, "Thompson"); + equal(newList[2].name, "Hyrum"); + equal(newList[2].lastName, "Thompson"); + + var ExtendedList = DefineList.extend({ testMe: function() { return "It Worked!"; } }); var myExtendedList = new ExtendedList(myArray); var newExtendedList = myExtendedList.map(function(person) { - person.lastName = "Thompson"; - return person; + person.lastName = "Thompson"; + return person; }); try { @@ -381,16 +381,16 @@ test('list.map', function() { test('list.sort a simple list', function() { - var myList = new DefineList([ - "Marshall", - "Austin", - "Hyrum" - ]); + var myList = new DefineList([ + "Marshall", + "Austin", + "Hyrum" + ]); myList.sort(); - equal(myList.length, 3); - equal(myList[0], "Austin"); + equal(myList.length, 3); + equal(myList[0], "Austin"); equal(myList[1], "Hyrum"); equal(myList[2], "Marshall", "Basic list was properly sorted."); }); @@ -418,70 +418,7 @@ test('list.sort a list of objects', function() { equal(objList[2].name, "Marshall", "List of objects was properly sorted."); }); -test('list.sort a list of DefineMaps', function() { - var Account = DefineMap.extend({ - name: "string", - amount: "number", - slug: { - serialize: true, - get: function() { - return this.name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); - } - } - }); - Account.List = DefineList.extend({ - "*": Account, - limit: "number", - skip: "number", - total: "number" - }); - - var accounts = new Account.List([ - { - name: "Savings", - amount: 20.00 - }, - { - name: "Checking", - amount: 103.24 - }, - { - name: "Kids Savings", - amount: 48155.13 - } - ]); - accounts.limit = 3; - - var template = stache('{{#each accounts}}{{name}},{{/each}}')({ accounts: accounts }); - equal(template.textContent, "Savings,Checking,Kids Savings,", "template rendered properly."); - - accounts.sort(function(a, b) { - if (a.name < b.name) { - return -1; - } else if (a.name > b.name) { - return 1; - } else { - return 0; - } - }); - equal(accounts.length, 3); - equal(template.textContent, "Checking,Kids Savings,Savings,", "template updated properly."); - - // Try sorting in reverse on the dynamic `slug` property - accounts.sort(function(a, b) { - if (a.slug < b.slug) { - return 1; - } else if (a.slug > b.slug) { - return -1; - } else { - return 0; - } - }); - equal(accounts.length, 3); - equal(accounts.limit, 3, "expandos still present after sorting/replacing."); - equal(template.textContent, "Savings,Kids Savings,Checking,", "template updated properly."); -}); test('list.sort a list of objects without losing reference (#137)', function() { var unSorted = new DefineList([ { id: 3 }, { id: 2 }, { id: 1 } ]); @@ -493,80 +430,80 @@ test('list.sort a list of objects without losing reference (#137)', function() { test("list defines", 6, function() { var Todo = function(props) { - assign(this, props); - CID(this); - }; + assign(this, props); + CID(this); + }; define(Todo.prototype, { - completed: "boolean", - destroyed: { - value: false - } - }); + completed: "boolean", + destroyed: { + value: false + } + }); Todo.prototype.destroy = function() { - this.destroyed = true; - }; - - var TodoList = DefineList.extend({ - - "*": Todo, - remaining: { - get: function() { - return this.filter({ - completed: false - }); - } - }, - completed: { - get: function() { - return this.filter({ - completed: true - }); - } - }, - - destroyCompleted: function() { - this.completed.forEach(function(todo) { - todo.destroy(); - }); - }, - setCompletedTo: function(value) { - this.forEach(function(todo) { - todo.completed = value; - }); - } - }); + this.destroyed = true; + }; + + var TodoList = DefineList.extend({ + + "*": Todo, + remaining: { + get: function() { + return this.filter({ + completed: false + }); + } + }, + completed: { + get: function() { + return this.filter({ + completed: true + }); + } + }, + + destroyCompleted: function() { + this.completed.forEach(function(todo) { + todo.destroy(); + }); + }, + setCompletedTo: function(value) { + this.forEach(function(todo) { + todo.completed = value; + }); + } + }); var todos = new TodoList([ { completed: true }, { completed: false } ]); - ok(todos.item(0) instanceof Todo, "correct instance"); - equal(todos.completed.length, 1, "only one todo"); + ok(todos.item(0) instanceof Todo, "correct instance"); + equal(todos.completed.length, 1, "only one todo"); todos.on("completed", function(ev, newVal, oldVal) { - ok(newVal instanceof TodoList, "right type"); - equal(newVal.length, 2, "all items"); - ok(oldVal instanceof TodoList, "right type"); - equal(oldVal.length, 1, "all items"); - }); + ok(newVal instanceof TodoList, "right type"); + equal(newVal.length, 2, "all items"); + ok(oldVal instanceof TodoList, "right type"); + equal(oldVal.length, 1, "all items"); + }); - todos.setCompletedTo(true); + todos.setCompletedTo(true); }); QUnit.test("extending the base supports overwriting _eventSetup", function() { - var L = DefineList.extend({}); + var L = DefineList.extend({}); Object.getOwnPropertyDescriptor(DefineMap.prototype, "_eventSetup"); - L.prototype.arbitraryProp = true; + L.prototype.arbitraryProp = true; ok(true, "set arbitraryProp"); L.prototype._eventSetup = function() {}; - ok(true, "worked"); + ok(true, "worked"); }); QUnit.test("setting expandos on a DefineList", function() { - var DL = DefineList.extend({ - count: "number" - }); + var DL = DefineList.extend({ + count: "number" + }); - var dl = new DL(); + var dl = new DL(); dl.set({ count: 5, skip: 2 }); QUnit.equal(dl.get("count"), 5, "read with .get defined"); //-> 5 @@ -581,37 +518,32 @@ QUnit.test("setting expandos on a DefineList", function() { QUnit.test("passing a DefineList to DefineList (#33)", function() { var m = new DefineList([ {}, {} ]); - var m2 = new DefineList(m); - QUnit.deepEqual(m.get(), m2.get()); - QUnit.ok(m[0] === m2[0], "index the same"); - QUnit.ok(m[1] === m2[1], "index the same"); + var m2 = new DefineList(m); + QUnit.deepEqual(m.get(), m2.get()); + QUnit.ok(m[0] === m2[0], "index the same"); + QUnit.ok(m[1] === m2[1], "index the same"); }); QUnit.test("reading and setting expandos", function() { - var list = new DefineList(); + var list = new DefineList(); var countObservation = new Observation(function() { - return list.get("count"); + return list.get("count"); }, null, function(newValue) { - QUnit.equal(newValue, 1000, "got new value"); - }); - countObservation.start(); + QUnit.equal(newValue, 1000, "got new value"); + }); + countObservation.start(); list.set("count", 1000); QUnit.equal(countObservation.value, 1000); - var list2 = new DefineList(); + var list2 = new DefineList(); list2.on("count", function(ev, newVal) { - QUnit.equal(newVal, 5); - }); - list2.set("count", 5); -}); - -QUnit.test("is list like", function() { - var list = new DefineList(); - QUnit.ok(types.isListLike(list)); + QUnit.equal(newVal, 5); + }); + list2.set("count", 5); }); QUnit.test("extending DefineList constructor functions (#61)", function() { @@ -622,25 +554,25 @@ QUnit.test("extending DefineList constructor functions (#61)", function() { var list = new CList([ {}, {} ]); list.on("aProp", function(ev, newVal, oldVal) { - QUnit.equal(newVal, "PROP"); - QUnit.equal(oldVal, undefined); - }); + QUnit.equal(newVal, "PROP"); + QUnit.equal(oldVal, undefined); + }); list.on("bProp", function(ev, newVal, oldVal) { - QUnit.equal(newVal, "FOO"); - QUnit.equal(oldVal, undefined); - }); + QUnit.equal(newVal, "FOO"); + QUnit.equal(oldVal, undefined); + }); list.on("cProp", function(ev, newVal, oldVal) { - QUnit.equal(newVal, "BAR"); - QUnit.equal(oldVal, undefined); - }); + QUnit.equal(newVal, "BAR"); + QUnit.equal(oldVal, undefined); + }); - list.aProp = "PROP"; - list.bProp = 'FOO'; - list.cProp = 'BAR'; + list.aProp = "PROP"; + list.bProp = 'FOO'; + list.cProp = 'BAR'; - QUnit.ok(list.aMethod); - QUnit.ok(list.bMethod); - QUnit.ok(list.cMethod); + QUnit.ok(list.aMethod); + QUnit.ok(list.bMethod); + QUnit.ok(list.cMethod); }); QUnit.test("extending DefineList constructor functions more than once (#61)", function() { @@ -654,71 +586,71 @@ QUnit.test("extending DefineList constructor functions more than once (#61)", fu var list2 = new CList([ {}, {}, {} ]); list1.on("aProp", function(ev, newVal, oldVal) { - QUnit.equal(newVal, "PROP", "aProp newVal on list1"); - QUnit.equal(oldVal, undefined); - }); + QUnit.equal(newVal, "PROP", "aProp newVal on list1"); + QUnit.equal(oldVal, undefined); + }); list1.on("bProp", function(ev, newVal, oldVal) { - QUnit.equal(newVal, "FOO", "bProp newVal on list1"); - QUnit.equal(oldVal, undefined); - }); + QUnit.equal(newVal, "FOO", "bProp newVal on list1"); + QUnit.equal(oldVal, undefined); + }); list2.on("aProp", function(ev, newVal, oldVal) { - QUnit.equal(newVal, "PROP", "aProp newVal on list2"); - QUnit.equal(oldVal, undefined); - }); + QUnit.equal(newVal, "PROP", "aProp newVal on list2"); + QUnit.equal(oldVal, undefined); + }); list2.on("cProp", function(ev, newVal, oldVal) { - QUnit.equal(newVal, "BAR", "cProp newVal on list2"); - QUnit.equal(oldVal, undefined); - }); - - list1.aProp = "PROP"; - list1.bProp = 'FOO'; - list2.aProp = "PROP"; - list2.cProp = 'BAR'; - QUnit.ok(list1.aMethod, "list1 aMethod"); - QUnit.ok(list1.bMethod); - QUnit.ok(list2.aMethod); - QUnit.ok(list2.cMethod, "list2 cMethod"); + QUnit.equal(newVal, "BAR", "cProp newVal on list2"); + QUnit.equal(oldVal, undefined); + }); + + list1.aProp = "PROP"; + list1.bProp = 'FOO'; + list2.aProp = "PROP"; + list2.cProp = 'BAR'; + QUnit.ok(list1.aMethod, "list1 aMethod"); + QUnit.ok(list1.bMethod); + QUnit.ok(list2.aMethod); + QUnit.ok(list2.cMethod, "list2 cMethod"); }); QUnit.test("extending DefineList constructor functions - value (#61)", function() { var AList = DefineList.extend("AList", { aProp: { value: 1 } }); - var BList = AList.extend("BList", { }); + var BList = AList.extend("BList", { }); var CList = BList.extend("CList", { }); - var c = new CList([]); + var c = new CList([]); QUnit.equal(c.aProp, 1, "got initial value"); }); QUnit.test("'*' inheritance works (#61)", function() { - var Account = DefineMap.extend({ - name: "string", - amount: "number", - slug: { - serialize: true, - get: function() { - return this.name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); - } - } - }); - - var BaseList = DefineList.extend({ - "*": Account - }); - - var ExtendedList = BaseList.extend({}); + var Account = DefineMap.extend({ + name: "string", + amount: "number", + slug: { + serialize: true, + get: function() { + return this.name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); + } + } + }); + + var BaseList = DefineList.extend({ + "*": Account + }); + + var ExtendedList = BaseList.extend({}); var xl = new ExtendedList([ {} ]); - QUnit.ok(xl[0] instanceof Account); + QUnit.ok(xl[0] instanceof Account); }); QUnit.test("shorthand getter setter (#56)", function() { - var People = DefineList.extend({ + var People = DefineList.extend({ first: "*", last: "*", get fullName() { @@ -732,7 +664,7 @@ QUnit.test("shorthand getter setter (#56)", function() { }); var p = new People([]); - p.fullName = "Mohamed Cherif"; + p.fullName = "Mohamed Cherif"; p.on("fullName", function(ev, newVal, oldVal) { QUnit.equal(oldVal, "Mohamed Cherif"); @@ -796,35 +728,35 @@ QUnit.test("added and removed are called after items are added/removed (#14)", f QUnit.test("* vs # (#78)", function() { - var MyList = DefineList.extend({ - "*": "number", - "#": { + var MyList = DefineList.extend({ + "*": "number", + "#": { added: function() { - ok(true, "called on init"); - }, + ok(true, "called on init"); + }, removed: function() {}, - type: "string" - } - }); + type: "string" + } + }); var list = new MyList([ 1, 2, 3 ]); - QUnit.ok(list[0] === "1", "converted to string"); - list.set("prop", "4"); - QUnit.ok(list.prop === 4, "type converted"); + QUnit.ok(list[0] === "1", "converted to string"); + list.set("prop", "4"); + QUnit.ok(list.prop === 4, "type converted"); }); QUnit.test("Array shorthand uses #", function() { - var MyMap = DefineMap.extend({ + var MyMap = DefineMap.extend({ "numbers": [ "number" ] - }); + }); var map = new MyMap({ numbers: [ "1", "2" ] }); - QUnit.ok(map.numbers[0] === 1, "converted to number"); + QUnit.ok(map.numbers[0] === 1, "converted to number"); - map.numbers.set("prop", "4"); - QUnit.ok(map.numbers.prop === "4", "type left alone"); + map.numbers.set("prop", "4"); + QUnit.ok(map.numbers.prop === "4", "type left alone"); }); QUnit.test("replace-with-self lists are diffed properly (can-view-live#10)", function() { @@ -1076,3 +1008,126 @@ test('reduceRight', function() { equal(concatenatedNames, "BobAlice", "ReduceRight works over list"); }); + +test("compute(defineMap, 'property.names') works (#20)", function(){ + var map = new DefineMap(); + var c = compute(map, "foo.bar"); + c.on("change", function(ev, newVal){ + QUnit.equal(newVal, 2); + }); + + map.set("foo", new DefineMap()); + map.foo.set("bar", 2); + +}); + +test("compute(DefineList, 0) works (#17)", function(assert){ + assert.expect(1); + var list = new DefineList([1,2,3]); + var c = compute(list, 0); + c.on("change", function(ev, newVal){ + assert.equal(newVal, 5); + }); + list.set(0, 5); +}); + +test("works with can-reflect", function(){ + var a = new DefineMap({ foo: 4 }); + var b = new DefineList([ "foo", "bar" ]); + var c; + QUnit.equal( canReflect.getKeyValue(b, "0"), "foo", "unbound value"); + + var handler = function(newValue){ + QUnit.equal(newValue, "quux", "observed new value"); + }; + QUnit.ok(!canReflect.isValueLike(b), "isValueLike is false"); + QUnit.ok(canReflect.isObservableLike(b), "isObservableLike is true"); + QUnit.ok(canReflect.isMapLike(b), "isMapLike is true"); + QUnit.ok(canReflect.isListLike(b), "isListLike is false"); + + QUnit.ok( !canReflect.keyHasDependencies(b, "length"), "keyHasDependencies -- false"); + + define(c = Object.create(b), { + length: { + get: function() { + return a.foo; + } + } + }); + + QUnit.ok(canReflect.getKeyDependencies(c, "length"), "dependencies exist"); + QUnit.ok( + canReflect.getKeyDependencies(c, "length").valueDependencies.has(c._computed.length.compute), + "dependencies returned" + ); + + canReflect.onKeysAdded(b, handler); + canReflect.onKeysRemoved(b, handler); + QUnit.ok(b.__bindEvents.add, "add handler added"); + QUnit.ok(b.__bindEvents.remove, "remove handler added"); + + b.push("quux"); + c.push("quux"); + QUnit.equal( canReflect.getKeyValue(c, "length"), "4", "bound value"); + b.pop(); + +}); + +QUnit.test("can-reflect setKeyValue", function(){ + var a = new DefineList([ "a", "b" ]); + + canReflect.setKeyValue(a, 1, "c"); + QUnit.equal(a[1], "c", "setKeyValue"); +}); + +QUnit.test("can-reflect deleteKeyValue", function(){ + var a = new DefineList([ "a", "b" ]); + a.set("foo", "bar"); + + canReflect.deleteKeyValue(a, 0); + QUnit.equal(a[1], undefined, "last value is now undefined"); + QUnit.equal(a[0], "b", "last value is shifted down"); + + canReflect.deleteKeyValue(a, "foo"); + QUnit.equal(a.foo, undefined, "value not included in serial"); + QUnit.ok(!("foo" in a.get()), "value not included in serial"); +}); + +QUnit.test("can-reflect getKeyDependencies", function() { + var a = new DefineMap({ foo: 4 }); + var b = new DefineList([ "foo", "bar" ]); + var c; + + ok(!canReflect.getKeyDependencies(b, "length"), "No dependencies before binding"); + + define(c = Object.create(b), { + length: { + get: function() { + return a.foo; + } + } + }); + + ok(canReflect.getKeyDependencies(c, "length"), "dependencies exist"); + ok(canReflect.getKeyDependencies(c, "length").valueDependencies.has(c._computed.length.compute), "dependencies returned"); + +}); + +QUnit.test("registered symbols", function() { + var a = new DefineMap({ "a": "a" }); + + ok(a[canSymbol.for("can.isMapLike")], "can.isMapLike"); + equal(a[canSymbol.for("can.getKeyValue")]("a"), "a", "can.getKeyValue"); + a[canSymbol.for("can.setKeyValue")]("a", "b"); + equal(a.a, "b", "can.setKeyValue"); + + function handler(val) { + equal(val, "c", "can.onKeyValue"); + } + + a[canSymbol.for("can.onKeyValue")]("a", handler); + a.a = "c"; + + a[canSymbol.for("can.offKeyValue")]("a", handler); + a.a = "d"; // doesn't trigger handler +}); diff --git a/list/list.js b/list/list.js index 3d9a000..6212946 100644 --- a/list/list.js +++ b/list/list.js @@ -11,10 +11,12 @@ var defineHelpers = require("../define-helpers/define-helpers"); var assign = require("can-util/js/assign/assign"); var diff = require("can-util/js/diff/diff"); var each = require("can-util/js/each/each"); -var isArray = require("can-util/js/is-array/is-array"); var makeArray = require("can-util/js/make-array/make-array"); var types = require("can-types"); var ns = require("can-namespace"); +var canReflect = require("can-reflect"); +var canSymbol = require("can-symbol"); +var CIDSet = require("can-util/js/cid-set/cid-set"); var splice = [].splice; var runningNative = false; @@ -257,15 +259,15 @@ var DefineList = Construct.extend("DefineList", } // otherwise we are setting multiple else { - if (isArray(prop)) { + if (canReflect.isListLike(prop)) { if (value) { this.replace(prop); } else { this.splice.apply(this, [ 0, prop.length ].concat(prop)); } } else { - each(prop, function(value, prop) { - this.set(prop, value); + canReflect.eachKey(prop, function(value, prop) { + canReflect.setKeyValue(this, prop, value); }, this); } } @@ -1131,11 +1133,11 @@ assign(DefineList.prototype, { // Go through each of the passed `arguments` and // see if it is list-like, an array, or something else each(arguments, function(arg) { - if (types.isListLike(arg) || Array.isArray(arg)) { + if (canReflect.isListLike(arg)) { // If it is list-like we want convert to a JS array then // pass each item of the array to this.__type - var arr = types.isListLike(arg) ? makeArray(arg) : arg; - each(arr, function(innerArg) { + var arr = Array.isArray(arg) ? arg : makeArray(arg); + arr.forEach(function(innerArg) { args.push(this.__type(innerArg)); }, this); } else { @@ -1307,6 +1309,19 @@ for (var prop in define.eventsProto) { writable: true }); } +// @@can.onKeyValue and @@can.offKeyValue are also on define.eventsProto +// but symbols are not enumerated in for...in loops +var eventsProtoSymbols = ("getOwnPropertySymbols" in Object) ? + Object.getOwnPropertySymbols(define.eventsProto) : + [canSymbol.for("can.onKeyValue"), canSymbol.for("can.offKeyValue")]; + +eventsProtoSymbols.forEach(function(sym) { + Object.defineProperty(DefineList.prototype, sym, { + enumerable:false, + value: define.eventsProto[sym], + writable: true + }); +}); Object.defineProperty(DefineList.prototype, "length", { get: function() { @@ -1336,12 +1351,11 @@ Object.defineProperty(DefineList.prototype, "length", { enumerable: true }); -var oldIsListLike = types.isListLike; -types.isListLike = function(obj) { - return obj instanceof DefineList || oldIsListLike.apply(this, arguments); -}; - -DefineList.prototype.each = DefineList.prototype.forEach; +Object.defineProperty(DefineList.prototype, "each", { + enumerable: false, + writable: true, + value: DefineList.prototype.forEach +}); DefineList.prototype.attr = function(prop, value) { canLog.warn("DefineMap::attr shouldn't be called"); if (arguments.length === 0) { @@ -1366,6 +1380,52 @@ DefineList.prototype.items = function() { return this.get(); }; +DefineList.prototype[canSymbol.for("can.getKeyValue")] = DefineList.prototype.get; +DefineList.prototype[canSymbol.for("can.setKeyValue")] = DefineList.prototype.set; +DefineList.prototype[canSymbol.for("can.deleteKeyValue")] = function(prop) { + if(typeof prop === "number") { + this.splice(prop, 1); + } else { + this.set(prop, undefined); + } + return this; +}; +DefineList.prototype[canSymbol.for("can.getValue")] = DefineList.prototype.get; +DefineList.prototype[canSymbol.for("can.setValue")] = DefineList.prototype.replace; +DefineList.prototype[canSymbol.for("can.isMapLike")] = true; +DefineList.prototype[canSymbol.for("can.isListLike")] = true; +DefineList.prototype[canSymbol.for("can.isValueLike")] = false; +DefineList.prototype[canSymbol.iterator] = function() { + var index = -1; + return { + next: function() { + index++; + return { + value: this[index], + done: index >= this.length + }; + }.bind(this) + }; +}; +DefineList.prototype[canSymbol.for("can.keyHasDependencies")] = function(key) { + return !!(this._computed && this._computed[key] && this._computed[key].compute); +}; +DefineList.prototype[canSymbol.for("can.getKeyDependencies")] = function(key) { + var ret; + if(this._computed && this._computed[key] && this._computed[key].compute) { + ret = {}; + ret.valueDependencies = new CIDSet(); + ret.valueDependencies.add(this._computed[key].compute); + } + return ret; +}; +DefineList.prototype[canSymbol.for("can.onKeysAdded")] = function(handler) { + this[canSymbol.for("can.onKeyValue")]("add", handler); +}; +DefineList.prototype[canSymbol.for("can.onKeysRemoved")] = function(handler) { + this[canSymbol.for("can.onKeyValue")]("remove", handler); +}; + types.DefineList = DefineList; types.DefaultList = DefineList; module.exports = ns.DefineList = DefineList; diff --git a/map/map-test.js b/map/map-test.js index 6fe42f5..6fc9bfd 100644 --- a/map/map-test.js +++ b/map/map-test.js @@ -3,10 +3,11 @@ var QUnit = require("steal-qunit"); var DefineMap = require("can-define/map/map"); var define = require("can-define"); var Observation = require("can-observation"); -var canTypes = require("can-types"); var each = require("can-util/js/each/each"); var compute = require("can-compute"); var assign = require("can-util/js/assign/assign"); +var canSymbol = require("can-symbol"); +var canReflect = require("can-reflect"); var sealWorks = (function() { try { var o = {}; @@ -295,11 +296,6 @@ QUnit.test("serialize: function works (#38)", function(){ }); -QUnit.test("isMapLike", function(){ - var map = new DefineMap({}); - ok(canTypes.isMapLike(map), "is map like"); -}); - QUnit.test("get will not create properties", function(){ var method = function(){}; var MyMap = DefineMap.extend({ @@ -665,6 +661,89 @@ QUnit.test(".value values are overwritten by props in DefineMap construction", f equal(foo.bar, "quux", "Value set properly"); }); +QUnit.test("can-reflect reflections work with DefineMap", function() { + var b = new DefineMap({ "foo": "bar" }); + var c = new (DefineMap.extend({ + "baz": { + get: function() { + return b.foo; + } + } + }))({ "foo": "bar", thud: "baz" }); + + QUnit.equal( canReflect.getKeyValue(b, "foo"), "bar", "unbound value"); + + var handler = function(newValue){ + QUnit.equal(newValue, "quux", "observed new value"); + + // Turn off the "foo" handler but "thud" should still be bound. + canReflect.offKeyValue(c, "baz", handler); + }; + QUnit.ok(!canReflect.isValueLike(c), "isValueLike is false"); + QUnit.ok(canReflect.isObservableLike(c), "isObservableLike is true"); + QUnit.ok(canReflect.isMapLike(c), "isMapLike is true"); + QUnit.ok(!canReflect.isListLike(c), "isListLike is false"); + + QUnit.ok( !canReflect.keyHasDependencies(b, "foo"), "keyHasDependencies -- false"); + + canReflect.onKeyValue(c, "baz", handler); + // Do a second binding to check that you can unbind correctly. + canReflect.onKeyValue(c, "thud", handler); + QUnit.ok( canReflect.keyHasDependencies(c, "baz"), "keyHasDependencies -- true"); + + b.foo = "quux"; + c.thud = "quux"; + + QUnit.equal( canReflect.getKeyValue(c, "baz"), "quux", "bound value"); + // sanity checks to ensure that handler doesn't get called again. + b.foo = "thud"; + c.baz = "jeek"; + +}); + +QUnit.test("can-reflect setKeyValue", function(){ + var a = new DefineMap({ "a": "b" }); + + canReflect.setKeyValue(a, "a", "c"); + QUnit.equal(a.a, "c", "setKeyValue"); +}); + +QUnit.test("can-reflect deleteKeyValue", function(){ + var a = new DefineMap({ "a": "b" }); + + canReflect.deleteKeyValue(a, "a"); + QUnit.equal(a.a, undefined, "value is now undefined"); + QUnit.ok(!("a" in a.get()), "value not included in serial"); +}); + +QUnit.test("can-reflect getKeyDependencies", function() { + var a = new DefineMap({ "a": "a" }); + var b = new (DefineMap.extend({ + "a": { + get: function() { + return a.a; + } + } + }))(); + + // DefineMaps bind automatically without events, so this is already running. + ok(canReflect.getKeyDependencies(b, "a"), "dependencies exist"); + ok(!canReflect.getKeyDependencies(b, "b"), "no dependencies exist for unknown value"); + ok(canReflect.getKeyDependencies(b, "a").valueDependencies.has(b._computed.a.compute), "dependencies returned"); + +}); + +QUnit.test("can-reflect setValue", function() { + var aData = { "a": "b" }; + var bData = { "b": "c" }; + + var a = new DefineMap(aData); + var b = new DefineMap(bData); + + a[canSymbol.for("can.setValue")](b); + QUnit.deepEqual(a.get(), assign(aData, bData), "when called with an object, should merge into existing object"); +}); + QUnit.test("Does not attempt to redefine _data if already defined", function() { var Bar = DefineMap.extend({seal: false}, { baz: { value : "thud" } diff --git a/map/map.js b/map/map.js index df17bc0..4422085 100644 --- a/map/map.js +++ b/map/map.js @@ -1,302 +1,356 @@ var Construct = require("can-construct"); var define = require("can-define"); var assign = require("can-util/js/assign/assign"); -var isArray = require("can-util/js/is-array/is-array"); -var isPlainObject = require("can-util/js/is-plain-object/is-plain-object"); var defineHelpers = require("../define-helpers/define-helpers"); var Observation = require("can-observation"); var types = require("can-types"); var canBatch = require("can-event/batch/batch"); var ns = require("can-namespace"); var canLog = require("can-util/js/log/log"); +var canReflect = require("can-reflect"); +var canSymbol = require("can-symbol"); +var CIDSet = require("can-util/js/cid-set/cid-set"); var readWithoutObserve = Observation.ignore(function(map, prop){ - return map[prop]; + return map[prop]; }); var eachDefinition = function(map, cb, thisarg, definitions, observe) { - for(var prop in definitions) { - var definition = definitions[prop]; - if(typeof definition !== "object" || ("serialize" in definition ? !!definition.serialize : !definition.get)) { + for(var prop in definitions) { + var definition = definitions[prop]; + if(typeof definition !== "object" || ("serialize" in definition ? !!definition.serialize : !definition.get)) { - var item = observe === false ? readWithoutObserve(map, prop) : map[prop]; + var item = observe === false ? readWithoutObserve(map, prop) : map[prop]; - if (cb.call(thisarg || item, item, prop, map) === false) { - return false; - } - } - } + if (cb.call(thisarg || item, item, prop, map) === false) { + return false; + } + } + } }; var setProps = function(props, remove) { props = defineHelpers.removeSpecialKeys(assign({}, props)); - var prop, - self = this, - newVal; + var prop, + self = this, + newVal; - // Batch all of the change events until we are done. - canBatch.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]; + // Batch all of the change events until we are done. + canBatch.start(); + // Merge current properties with the new ones. + canReflect.eachKey(this, function(curVal, prop) { + //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[prop] = undefined; - } - return; - } - if( typeof curVal !== "object" || curVal === null ) { - self.set(prop, newVal); - } - else if( ("replace" in curVal) && isArray(newVal)) { - curVal.replace(newVal); - } - else if( ("set" in curVal) && (isPlainObject(newVal) || isArray(newVal))) { - curVal.set(newVal, remove); - } - else if( ("attr" in curVal) && (isPlainObject(newVal) || isArray(newVal)) ) { - curVal.attr(newVal, remove); - } - else if(curVal !== newVal) { - self.set(prop, newVal); - } - delete props[prop]; - }, this, false); - // Add remaining props. - for (prop in props) { - // Ignore _cid. - if (prop !== "_cid") { - newVal = props[prop]; - this.set(prop, newVal); - } + // If we are merging, remove the property if it has no value. + if (newVal === undefined) { + if (remove) { + self[prop] = undefined; + } + return; + } + if(canReflect.isValueLike(curVal)) { + //if( typeof curVal !== "object" || curVal === null ) { + canReflect.setKeyValue(self, prop, newVal); + } + else if( canReflect.isMapLike(curVal) || canReflect.isListLike( curVal ) ) { + canReflect.setValue(curVal, newVal); + } + else if(curVal !== newVal) { + canReflect.setKeyValue(self, prop, newVal); + } + delete props[prop]; + }, this, false); + // Add remaining props. + for (prop in props) { + // Ignore _cid. + if (prop !== "_cid") { + newVal = props[prop]; + canReflect.setKeyValue(this, prop, newVal); + } - } - canBatch.stop(); - return this; + } + canBatch.stop(); + return this; }; var DefineMap = Construct.extend("DefineMap",{ - setup: function(base){ + setup: function(base){ var key, prototype = this.prototype; - if(DefineMap) { - define(prototype, prototype, base.prototype._define); + if(DefineMap) { + define(prototype, prototype, base.prototype._define); for(key in DefineMap.prototype) { define.defineConfigurableAndNotEnumerable(prototype, key, prototype[key]); } - this.prototype.setup = function(props){ + this.prototype.setup = function(props){ define.setup.call( this, defineHelpers.removeSpecialKeys(defineHelpers.toObject(this, props,{}, DefineMap)), this.constructor.seal ); - }; + }; } else { for(key in prototype) { define.defineConfigurableAndNotEnumerable(prototype, key, prototype[key]); - } - } + } + } define.defineConfigurableAndNotEnumerable(prototype, "constructor", this); } },{ - // setup for only dynamic DefineMap instances - setup: function(props, sealed){ - if(!this._define) { - Object.defineProperty(this,"_define",{ - enumerable: false, - value: { - definitions: {} - } - }); - Object.defineProperty(this,"_data",{ - enumerable: false, - value: {} - }); - } + // setup for only dynamic DefineMap instances + setup: function(props, sealed){ + if(!this._define) { + Object.defineProperty(this,"_define",{ + enumerable: false, + value: { + definitions: {} + } + }); + Object.defineProperty(this,"_data",{ + enumerable: false, + value: {} + }); + } define.setup.call( this, defineHelpers.removeSpecialKeys(defineHelpers.toObject(this, props,{}, DefineMap)), sealed === true ); - }, - /** - * @function can-define/map/map.prototype.get get - * @parent can-define/map/map.prototype - * - * @description Get a value or all values from a DefineMap. - * - * @signature `map.get()` - * - * Returns a plain JavaScript object that contains the properties and values of the map instance. Any property values - * that also have a `get` method will have their `get` method called and the resulting value will be used as - * the property value. This can be used to recursively convert a map instance to an object of other plain - * JavaScript objects. Cycles are supported and only create one object. - * - * `.get()` can still return other non plain JS objects like Date. - * Use [can-define/map/map.prototype.serialize] when a form proper for `JSON.stringify` is needed. - * - * ```js - * var map = new DefineMap({foo: new DefineMap({bar: "zed"})}); - * map.get() //-> {foo: {bar: "zed"}}; - * ``` - * - * @return {Object} A plain JavaScript `Object` that contains all the properties and values of the map instance. - * - * @signature `map.get(propName)` - * - * Get a single property on a DefineMap instance. - * - * `.get(propName)` only should be used when reading properties that might not have been defined yet, but - * will be later via [can-define/map/map.prototype.set]. - * - * ```js - * var map = new DefineMap(); - * map.get("name") //-> undefined; - * ``` - * - * @param {String} propName The property name of a property that may not have been defined yet. - * @return {*} The value of that property. - */ - get: function(prop){ - if(prop) { - var value = this[prop]; - if(value !== undefined || prop in this || Object.isSealed(this)) { - return value; - } else { - Observation.add(this, prop); - return this[prop]; - } + }, + /** + * @function can-define/map/map.prototype.get get + * @parent can-define/map/map.prototype + * + * @description Get a value or all values from a DefineMap. + * + * @signature `map.get()` + * + * Returns a plain JavaScript object that contains the properties and values of the map instance. Any property values + * that also have a `get` method will have their `get` method called and the resulting value will be used as + * the property value. This can be used to recursively convert a map instance to an object of other plain + * JavaScript objects. Cycles are supported and only create one object. + * + * `.get()` can still return other non plain JS objects like Date. + * Use [can-define/map/map.prototype.serialize] when a form proper for `JSON.stringify` is needed. + * + * ```js + * var map = new DefineMap({foo: new DefineMap({bar: "zed"})}); + * map.get() //-> {foo: {bar: "zed"}}; + * ``` + * + * @return {Object} A plain JavaScript `Object` that contains all the properties and values of the map instance. + * + * @signature `map.get(propName)` + * + * Get a single property on a DefineMap instance. + * + * `.get(propName)` only should be used when reading properties that might not have been defined yet, but + * will be later via [can-define/map/map.prototype.set]. + * + * ```js + * var map = new DefineMap(); + * map.get("name") //-> undefined; + * ``` + * + * @param {String} propName The property name of a property that may not have been defined yet. + * @return {*} The value of that property. + */ + get: function(prop){ + if(prop) { + var value = this[prop]; + if(value !== undefined || prop in this || Object.isSealed(this)) { + return value; + } else { + Observation.add(this, prop); + return this[prop]; + } - } else { - return defineHelpers.serialize(this, 'get', {}); - } - }, - /** - * @function can-define/map/map.prototype.set set - * @parent can-define/map/map.prototype - * - * @description Sets multiple properties on a map instance or a property that wasn't predefined. - * - * @signature `map.set(props [,removeProps])` - * - * Assigns each value in `props` to a property on this map instance named after the - * corresponding key in `props`, effectively merging `props` into the Map. If `removeProps` is true, properties not in - * `props` will be set to `undefined`. - * - * @param {Object} props A collection of key-value pairs to set. - * If any properties already exist on the map, they will be overwritten. - * - * @param {Boolean} [removeProps=false] Whether to set keys not present in `props` to `undefined`. - * - * @return {can-define/map/map} The map instance for chaining. - * - * @signature `map.set(propName, value)` - * - * Assigns _value_ to a property on this map instance called _propName_. This will define - * the property if it hasn't already been predefined. - * - * @param {String} propName The property to set. - * @param {*} value The value to assign to `propName`. - * @return {can-define/map/map} This map instance, for chaining. - */ - set: function(prop, value){ - if(typeof prop === "object") { - return setProps.call(this, prop, value); - } - var defined = defineHelpers.defineExpando(this, prop, value); - if(!defined) { - this[prop] = value; - } - return this; - }, - /** - * @function can-define/map/map.prototype.serialize serialize - * @parent can-define/map/map.prototype - * - * @description Get a serialized representation of the map instance and its children. - * - * @signature `map.serialize()` - * - * Get the serialized Object form of the map. Serialized - * data is typically used to send back to a server. Use [can-define.types.serialize] - * to customize a property's serialized value or if the property should be added to - * the result or not. - * - * `undefined` serialized values are not added to the result. - * - * ```js - * var MyMap = DefineMap.extend({ - * date: { - * type: "date", - * serialize: function(date){ - * return date.getTime() - * } - * } - * }); - * - * var myMap = new MyMap({date: new Date(), count: 5}); - * myMap.serialize() //-> {date: 1469566698504, count: 5} - * ``` - * - * @return {Object} A JavaScript Object that can be serialized with `JSON.stringify` or other methods. - * - */ - serialize: function () { - return defineHelpers.serialize(this, 'serialize', {}); - }, + } else { + return defineHelpers.serialize(this, 'get', {}); + } + }, + /** + * @function can-define/map/map.prototype.set set + * @parent can-define/map/map.prototype + * + * @description Sets multiple properties on a map instance or a property that wasn't predefined. + * + * @signature `map.set(props [,removeProps])` + * + * Assigns each value in `props` to a property on this map instance named after the + * corresponding key in `props`, effectively merging `props` into the Map. If `removeProps` is true, properties not in + * `props` will be set to `undefined`. + * + * @param {Object} props A collection of key-value pairs to set. + * If any properties already exist on the map, they will be overwritten. + * + * @param {Boolean} [removeProps=false] Whether to set keys not present in `props` to `undefined`. + * + * @return {can-define/map/map} The map instance for chaining. + * + * @signature `map.set(propName, value)` + * + * Assigns _value_ to a property on this map instance called _propName_. This will define + * the property if it hasn't already been predefined. + * + * @param {String} propName The property to set. + * @param {*} value The value to assign to `propName`. + * @return {can-define/map/map} This map instance, for chaining. + */ + set: function(prop, value){ + if(typeof prop === "object") { + return setProps.call(this, prop, value); + } + var defined = defineHelpers.defineExpando(this, prop, value); + if(!defined) { + this[prop] = value; + } + return this; + }, + /** + * @function can-define/map/map.prototype.serialize serialize + * @parent can-define/map/map.prototype + * + * @description Get a serialized representation of the map instance and its children. + * + * @signature `map.serialize()` + * + * Get the serialized Object form of the map. Serialized + * data is typically used to send back to a server. Use [can-define.types.serialize] + * to customize a property's serialized value or if the property should be added to + * the result or not. + * + * `undefined` serialized values are not added to the result. + * + * ```js + * var MyMap = DefineMap.extend({ + * date: { + * type: "date", + * serialize: function(date){ + * return date.getTime() + * } + * } + * }); + * + * var myMap = new MyMap({date: new Date(), count: 5}); + * myMap.serialize() //-> {date: 1469566698504, count: 5} + * ``` + * + * @return {Object} A JavaScript Object that can be serialized with `JSON.stringify` or other methods. + * + */ + serialize: function () { + return defineHelpers.serialize(this, 'serialize', {}); + }, - forEach: function(cb, thisarg, observe){ - if(observe !== false) { - Observation.add(this, '__keys'); - } - var res; - var constructorDefinitions = this._define.definitions; - if(constructorDefinitions) { - res = eachDefinition(this, cb, thisarg, constructorDefinitions, observe); - } - if(res === false) { - return this; - } - if(this._instanceDefinitions) { - eachDefinition(this, cb, thisarg, this._instanceDefinitions, observe); - } + forEach: function(cb, thisarg, observe){ + if(observe !== false) { + Observation.add(this, '__keys'); + } + var res; + var constructorDefinitions = this._define.definitions; + if(constructorDefinitions) { + res = eachDefinition(this, cb, thisarg, constructorDefinitions, observe); + } + if(res === false) { + return this; + } + if(this._instanceDefinitions) { + eachDefinition(this, cb, thisarg, this._instanceDefinitions, observe); + } - return this; - }, - "*": { - type: define.types.observable - } + return this; + }, + "*": { + type: define.types.observable + } }); +DefineMap.prototype[canSymbol.for("can.getKeyValue")] = DefineMap.prototype.get; +DefineMap.prototype[canSymbol.for("can.setKeyValue")] = DefineMap.prototype.set; +DefineMap.prototype[canSymbol.for("can.deleteKeyValue")] = function(prop) { + this.set(prop, undefined); + return this; +}; +DefineMap.prototype[canSymbol.for("can.getValue")] = DefineMap.prototype.get; +DefineMap.prototype[canSymbol.for("can.setValue")] = function(prop, value) { + if(typeof prop === "object") { + if (prop[canSymbol.for("can.getValue")]) { + this.set(canReflect.getValue(prop)); + } else { + this.set(prop); + } + } else { + if (value[canSymbol.for("can.getValue")]) { + this.set(prop, canReflect.getValue(value)); + } else { + this.set(prop, value); + } + } +}; +DefineMap.prototype[canSymbol.for("can.isMapLike")] = true; +DefineMap.prototype[canSymbol.for("can.isListLike")] = false; +DefineMap.prototype[canSymbol.for("can.isValueLike")] = false; +DefineMap.prototype[canSymbol.iterator] = function() { + return new define.Iterator(this); +}; +DefineMap.prototype[canSymbol.for("can.keyHasDependencies")] = function(key) { + return !!(this._computed && this._computed[key] && this._computed[key].compute); +}; +DefineMap.prototype[canSymbol.for("can.getKeyDependencies")] = function(key) { + var ret; + if(this._computed && this._computed[key] && this._computed[key].compute) { + ret = {}; + ret.valueDependencies = new CIDSet(); + ret.valueDependencies.add(this._computed[key].compute); + } + return ret; +}; // Add necessary event methods to this object. for(var prop in define.eventsProto) { DefineMap[prop] = define.eventsProto[prop]; - Object.defineProperty(DefineMap.prototype, prop, { - enumerable:false, - value: define.eventsProto[prop], - writable: true - }); + Object.defineProperty(DefineMap.prototype, prop, { + enumerable:false, + value: define.eventsProto[prop], + writable: true + }); } +// @@can.onKeyValue and @@can.offKeyValue are also on define.eventsProto +// but symbols are not enumerated in for...in loops +var eventsProtoSymbols = ("getOwnPropertySymbols" in Object) ? + Object.getOwnPropertySymbols(define.eventsProto) : + [canSymbol.for("can.onKeyValue"), canSymbol.for("can.offKeyValue")]; + +eventsProtoSymbols.forEach(function(sym) { + Object.defineProperty(DefineMap.prototype, sym, { + enumerable:false, + value: define.eventsProto[sym], + writable: true + }); +}); + types.DefineMap = DefineMap; types.DefaultMap = DefineMap; -DefineMap.prototype.toObject = function(){ - canLog.warn("Use DefineMap::get instead of DefineMap::toObject"); - return this.get(); -}; -DefineMap.prototype.each = DefineMap.prototype.forEach; - -var oldIsMapLike = types.isMapLike; -types.isMapLike = function(obj){ - return obj instanceof DefineMap || oldIsMapLike.apply(this, arguments); -}; +Object.defineProperty(DefineMap.prototype, "toObject", { + enumerable: false, + writable: true, + value: function(){ + canLog.warn("Use DefineMap::get instead of DefineMap::toObject"); + return this.get(); + } +}); +Object.defineProperty(DefineMap.prototype, "each", { + enumerable: false, + writable: true, + value: DefineMap.prototype.forEach +}); module.exports = ns.DefineMap = DefineMap; diff --git a/package.json b/package.json index 624726a..499b302 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "can-define", - "version": "1.0.26", + "version": "1.2.0-pre.9", "description": "Create observable objects with JS dot operator compatibility", "main": "can-define.js", "scripts": { @@ -8,8 +8,8 @@ "jshint": "jshint --config .jshintrc --exclude ./node_modules,./dist .", "preversion": "npm test && npm run build", "version": "git commit -am \"Update dist for release\" && git checkout -b release && git add -f dist/", - "postversion": "git push --tags && git checkout master && git branch -D release && git push", - "release:pre": "npm version prerelease && npm publish", + "postversion": "git push --tags && git checkout can-reflect && git branch -D release && git push origin can-reflect", + "release:pre": "npm version prerelease && npm publish --tag pre", "release:patch": "npm version patch && npm publish", "release:minor": "npm version minor && npm publish", "release:major": "npm version major && npm publish", @@ -32,17 +32,17 @@ "homepage": "https://github.com/canjs/can-define", "dependencies": { "can-cid": "^1.0.0", - "can-compute": "^3.0.0", - "can-construct": "^3.0.6", - "can-event": "^3.3.0", + "can-compute": "^3.1.0-pre.13", + "can-construct": "^3.2.0-pre.0", + "can-event": "^3.5.0-pre.2", "can-namespace": "^1.0.0", - "can-observation": "^3.0.1", - "can-types": "^1.0.1", - "can-util": "^3.5.1" + "can-observation": "^3.2.0-pre.15", + "can-reflect": "^1.0.0-pre.2", + "can-symbol": "^1.0.0-pre.0", + "can-types": "^1.1.0-pre.1", + "can-util": "^3.9.0-pre.4" }, "devDependencies": { - "can-list": "^3.0.1", - "can-stache": "^3.0.13", "jshint": "^2.9.1", "serve": "^5.1.4", "steal": "^1.0.7",