diff --git a/README.md b/README.md index e20d2f9..bd07f08 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,16 @@ expect(foo.a.b).toBe(50); // new one updated expect(a.b).toBe(30); // from before it was orphaned ``` +### Strings + +String concatenation is straightforward. + +```javascript +var object = {name: "world"}; +bind(object, "greeting", {"<-": "'hello ' + name + '!'"}); +expect(object.greeting).toBe("hello world!"); +``` + ### Sum Some advanced queries are possible with one-way bindings from @@ -858,7 +868,7 @@ expect(object.hasNeedle).toBe(true); `has` bindings are not incremental, but with the right data-structure, updates are cheap. The [Collections][] package contains Lists, Sets, -and OrderedSets that all can send content change notifications and thus +and OrderedSets that all can send ranged content change notifications and thus can be bound. ```javascript @@ -868,6 +878,19 @@ object.haystack = new Set([1, 2, 3]); expect(object.hasNeedle).toBe(true); ``` +Likewise, Maps implement `addMapChangeListener`, so you can use a `has` binding +to observe whether an entry exists with the given key. + +```javascript +// Continued from above... +var Map = require("collections/map"); +object.haystack = new Map([[1, "a"], [2, "b"]]); +object.needle = 2; +expect(object.hasNeedle).toBe(true); +object.needle = 3; +expect(object.hasNeedle).toBe(false); +``` + `has` bindings can also be left-to-right and bi-directional. ```javascript diff --git a/binders.js b/binders.js index 8d6ffc4..c417ea6 100644 --- a/binders.js +++ b/binders.js @@ -1,8 +1,6 @@ var Scope = require("./scope"); var Observers = require("./observers"); -var autoCancelPrevious = Observers.autoCancelPrevious; -var once = Observers.once; var observeRangeChange = Observers.observeRangeChange; var cancelEach = Observers.cancelEach; var makeNotObserver = Observers.makeNotObserver; @@ -20,7 +18,7 @@ function getStackTrace() { exports.bindProperty = bindProperty; var _bindProperty = bindProperty; // to bypass scope shadowing problems below function bindProperty(object, key, observeValue, source, descriptor, trace) { - return observeValue(autoCancelPrevious(function replaceBoundPropertyValue(value) { + return observeValue(function replaceBoundPropertyValue(value) { if (descriptor.isActive) { return; } @@ -36,30 +34,30 @@ function bindProperty(object, key, observeValue, source, descriptor, trace) { } finally { descriptor.isActive = false; } - }), source); + }, source); } exports.makePropertyBinder = makePropertyBinder; function makePropertyBinder(observeObject, observeKey) { return function bindProperty(observeValue, source, target, descriptor, trace) { - return observeKey(autoCancelPrevious(function replaceKey(key) { + return observeKey(function replaceKey(key) { if (key == null) return; - return observeObject(autoCancelPrevious(function replaceObject(object) { + return observeObject(function replaceObject(object) { if (object == null) return; if (object.bindProperty) { return object.bindProperty(key, observeValue, source, descriptor, trace); } else { return _bindProperty(object, key, observeValue, source, descriptor, trace); } - }), target); - }), target); + }, target); + }, target); }; } exports.bindGet = bindGet; var _bindGet = bindGet; // to bypass scope shadowing below function bindGet(collection, key, observeValue, source, descriptor, trace) { - return observeValue(autoCancelPrevious(function replaceValue(value) { + return observeValue(function replaceValue(value) { if (descriptor.isActive) { return; } @@ -70,30 +68,31 @@ function bindGet(collection, key, observeValue, source, descriptor, trace) { } finally { descriptor.isActive = false; } - }), source); + }, source); } exports.makeGetBinder = makeGetBinder; function makeGetBinder(observeCollection, observeKey) { return function bindGet(observeValue, source, target, descriptor, trace) { - return observeCollection(autoCancelPrevious(function replaceCollection(collection) { + return observeCollection(function replaceCollection(collection) { if (!collection) return; - return observeKey(autoCancelPrevious(function replaceKey(key) { + return observeKey(function replaceKey(key) { if (key == null) return; return _bindGet(collection, key, observeValue, source, descriptor, trace); - }), target); - }), target); + }, target); + }, target); }; } exports.makeHasBinder = makeHasBinder; function makeHasBinder(observeSet, observeValue) { return function bindHas(observeHas, source, target, descriptor, trace) { - return observeSet(autoCancelPrevious(function replaceHasBindingSet(set) { + return observeSet(function replaceHasBindingSet(set) { if (!set) return; - return observeValue(autoCancelPrevious(function replaceHasBindingValue(value) { + var equals = set.contentEquals || Object.equals; + return observeValue(function replaceHasBindingValue(value) { if (value == null) return; - return observeHas(autoCancelPrevious(function changeWhetherSetHas(has) { + return observeHas(function changeWhetherSetHas(has) { // wait for the initial value to be updated by the // other-way binding if (has) { // should be in set @@ -102,14 +101,37 @@ function makeHasBinder(observeSet, observeValue) { set.add(value); } } else { // should not be in set - while ((set.has || set.contains).call(set, value)) { + // Not all collections are sets. + // To enforce the rule that the collection does not + // contain the value, we must remove all of the + // equivalent values. + // However, we cannot use a simple while loop because + // certain collections refuse to effect the requested + // change, notably the MontageJS range controller + // content. + // So instead we count the values and remove that many, + // regardless of whether the changes take effect. + var count; + if (set.filter) { + // But not all collections implement filter, + // either. + count = set.filter(function (other) { + return equals(value, other); + }).length; + } else { + // But all collections must either implement has or + // contains, so we coerce the boolean to 0 or 1 + // count. + count = +(set.has || set.contains).call(set, value); + } + for (var index = 0; index < count; index++) { trace && console.log("REMOVE", value, "FROM", trace.targetPath, "BECAUSE", trace.sourcePath, getStackTrace()); (set.remove || set['delete']).call(set, value); } } - }), source); - }), target); - }), target); + }, source); + }, target); + }, target); }; } @@ -118,16 +140,17 @@ exports.makeEqualityBinder = makeEqualityBinder; function makeEqualityBinder(bindLeft, observeRight) { return function bindEquals(observeEquals, source, target, descriptor, trace) { // c - return observeEquals(autoCancelPrevious(function changeWhetherEquals(equals) { + return observeEquals(function changeWhetherEquals(equals) { if (equals) { trace && console.log("BIND", trace.targetPath, "TO", trace.sourcePath, getStackTrace()); // a <-> b - var cancel = bindLeft(observeRight, source, source, descriptor, trace); + var cancel = bindLeft(observeRight, source, source, descriptor, trace) || Function.noop; return function cancelEqualityBinding() { + cancel(); trace && console.log("UNBIND", trace.targetPath, "FROM", trace.sourcePath, getStackTrace()); }; } - }), target); + }, target); }; } @@ -135,24 +158,25 @@ function makeEqualityBinder(bindLeft, observeRight) { exports.makeEveryBlockBinder = makeEveryBlockBinder; function makeEveryBlockBinder(observeCollection, bindCondition, observeValue) { return function bindEveryBlock(observeEveryCondition, source, target, descriptor, trace) { - return observeEveryCondition(autoCancelPrevious(function replaceCondition(condition) { + return observeEveryCondition(function replaceCondition(condition) { if (!condition) return; - return observeCollection(autoCancelPrevious(function replaceCollection(collection) { + return observeCollection(function replaceCollection(collection) { if (!collection) return; var cancelers = []; function rangeChange(plus, minus, index) { + cancelEach(cancelers.slice(index, index + minus.length)); cancelers.swap(index, minus.length, plus.map(function (value, offset) { var scope = target.nest(value); return bindCondition(observeValue, scope, scope, descriptor, trace); })); } - var cancelRangeChange = observeRangeChange(collection, rangeChange, target); + var cancelRangeChange = observeRangeChange(collection, rangeChange, target) || Function.noop; return function cancelEveryBinding() { cancelEach(cancelers); cancelRangeChange(); }; - }), target); - }), source); + }, target); + }, source); }; }; @@ -161,11 +185,11 @@ function makeAndBinder(bindLeft, bindRight, observeLeft, observeRight, observeLe var observeNotRight = makeNotObserver(observeRight); var observeLeftAndNotRight = makeAndObserver(observeLeft, observeNotRight); return function bindEveryBlock(observeAndCondition, source, target, descriptor, trace) { - return observeAndCondition(autoCancelPrevious(function replaceAndCondition(condition) { + return observeAndCondition(function replaceAndCondition(condition) { if (condition == null) { } else if (condition) { - var cancelLeft = bindLeft(observeLeftBind, trueScope, target, descriptor, trace); - var cancelRight = bindRight(observeRightBind, trueScope, target, descriptor, trace); + var cancelLeft = bindLeft(observeLeftBind, trueScope, target, descriptor, trace) || Function.noop; + var cancelRight = bindRight(observeRightBind, trueScope, target, descriptor, trace) || Function.noop; return function cancelAndBinding() { cancelLeft(); cancelRight(); @@ -173,7 +197,7 @@ function makeAndBinder(bindLeft, bindRight, observeLeft, observeRight, observeLe } else { return bindLeft(observeLeftAndNotRight, target, target, descriptor, trace); } - }), source); + }, source); }; } @@ -182,18 +206,19 @@ function makeOrBinder(bindLeft, bindRight, observeLeft, observeRight, observeLef var observeNotRight = makeNotObserver(observeRight); var observeLeftOrNotRight = makeOrObserver(observeLeft, observeNotRight); return function bindEveryBlock(observeOrCondition, source, target, descriptor, trace) { - return observeOrCondition(autoCancelPrevious(function replaceOrCondition(condition) { + return observeOrCondition(function replaceOrCondition(condition) { if (condition == null) { } else if (!condition) { - var cancelLeft = bindLeft(observeLeftBind, falseScope, target, descriptor, trace); - var cancelRight = bindRight(observeRightBind, falseScope, target, descriptor, trace); return function cancelOrBinding() { + var cancelLeft = bindLeft(observeLeftBind, falseScope, target, descriptor, trace) || Function.noop; + var cancelRight = bindRight(observeRightBind, falseScope, target, descriptor, trace) || Function.noop; + return function cancelOrBinding() { cancelLeft(); cancelRight(); }; } else { return bindLeft(observeLeftOrNotRight, target, target, descriptor, trace); } - }), source); + }, source); }; } @@ -202,7 +227,7 @@ exports.makeConditionalBinder = makeConditionalBinder; function makeConditionalBinder(observeCondition, bindConsequent, bindAlternate) { return function bindCondition(observeSource, source, target, descriptor, trace) { // a - return observeCondition(autoCancelPrevious(function replaceCondition(condition) { + return observeCondition(function replaceCondition(condition) { if (condition == null) return; if (condition) { // b <- d @@ -211,7 +236,7 @@ function makeConditionalBinder(observeCondition, bindConsequent, bindAlternate) // c <- d return bindAlternate(observeSource, source, target, descriptor, trace); } - }), source); + }, source); }; } @@ -219,9 +244,9 @@ function makeConditionalBinder(observeCondition, bindConsequent, bindAlternate) exports.makeOnlyBinder = makeOnlyBinder; function makeOnlyBinder(observeCollection) { return function bindOnly(observeValue, sourceScope, targetScope, descriptor, trace) { - return observeCollection(autoCancelPrevious(function replaceCollection(collection) { + return observeCollection(function replaceCollection(collection) { if (!collection) return; - return observeValue(autoCancelPrevious(function replaceOnlyValue(value) { + return observeValue(function replaceOnlyValue(value) { if (value == null) return; if (collection.splice) { collection.splice(0, collection.length, value); @@ -229,8 +254,8 @@ function makeOnlyBinder(observeCollection) { collection.clear(); collection.add(value); } - }), sourceScope); - }), targetScope); + }, sourceScope); + }, targetScope); }; } @@ -239,9 +264,9 @@ function makeOnlyBinder(observeCollection) { exports.makeOneBinder = makeOneBinder; function makeOneBinder(observeCollection) { return function bindOne(observeValue, sourceScope, targetScope, descriptor, trace) { - return observeCollection(autoCancelPrevious(function replaceCollection(collection) { + return observeCollection(function replaceCollection(collection) { if (!collection) return; - return observeValue(autoCancelPrevious(function replaceOneValue(value) { + return observeValue(function replaceOneValue(value) { if (value == null) return; // FIXME: this is debatable. If set to its current value, do we clear the rest of the collection? @@ -255,8 +280,8 @@ function makeOneBinder(observeCollection) { collection.clear(); collection.add(value); } - }), sourceScope); - }), targetScope); + }, sourceScope); + }, targetScope); }; } @@ -264,7 +289,7 @@ function makeOneBinder(observeCollection) { exports.makeRangeContentBinder = makeRangeContentBinder; function makeRangeContentBinder(observeTarget, bindTarget) { return function bindRangeContent(observeSource, sourceScope, targetScope, descriptor, trace) { - return observeTarget(autoCancelPrevious(function replaceRangeContentTarget(target) { + return observeTarget(function replaceRangeContentTarget(target) { if (!target) { return bindTarget( Observers.makeLiteralObserver([]), @@ -275,7 +300,7 @@ function makeRangeContentBinder(observeTarget, bindTarget) { ); } - return observeSource(autoCancelPrevious(function replaceRangeContentSource(source) { + return observeSource(function replaceRangeContentSource(source) { if (source === target) { return; } @@ -303,20 +328,20 @@ function makeRangeContentBinder(observeTarget, bindTarget) { source.addRangeChangeListener(rangeContentSourceRangeChange); rangeContentSourceRangeChange(Array.from(source), Array.from(target), 0); - return once(function cancelRangeContentBinding() { + return function cancelRangeContentBinding() { source.removeRangeChangeListener(rangeContentSourceRangeChange); - }); - }), sourceScope); - }), targetScope); + }; + }, sourceScope); + }, targetScope); }; } exports.makeMapContentBinder = makeMapContentBinder; function makeMapContentBinder(observeTarget) { return function bindMapContent(observeSource, source, target, descriptor, trace) { - return observeTarget(autoCancelPrevious(function replaceMapContentBindingTarget(target) { + return observeTarget(function replaceMapContentBindingTarget(target) { if (!target) return; - return observeSource(autoCancelPrevious(function replaceMapContentBindingSource(source) { + return observeSource(function replaceMapContentBindingSource(source) { if (!source) { target.clear(); return; @@ -350,8 +375,8 @@ function makeMapContentBinder(observeTarget) { target.clear(); source.forEach(mapChange); return source.addMapChangeListener(mapChange); - }), source); - }), target); + }, source); + }, target); }; } @@ -359,9 +384,9 @@ function makeMapContentBinder(observeTarget) { exports.makeReversedBinder = makeReversedBinder; function makeReversedBinder(observeTarget) { return function bindReversed(observeSource, source, target, descriptor, trace) { - return observeTarget(autoCancelPrevious(function replaceReversedBindingTarget(target) { + return observeTarget(function replaceReversedBindingTarget(target) { if (!target) return; - return observeSource(autoCancelPrevious(function replaceReversedBindingSource(source) { + return observeSource(function replaceReversedBindingSource(source) { if (!source) { target.clear(); return; @@ -375,18 +400,18 @@ function makeReversedBinder(observeTarget) { } source.addRangeChangeListener(rangeChange); rangeChange(source, target, 0); - return once(function cancelReversedBinding() { + return function cancelReversedBinding() { source.removeRangeChangeListener(rangeChange); - }); - }), source); - }), target); + }; + }, source); + }, target); }; } exports.makeDefinedBinder = makeDefinedBinder; function makeDefinedBinder(bindTarget) { return function bindReversed(observeSource, sourceScope, targetScope, descriptor, trace) { - return observeSource(autoCancelPrevious(function replaceSource(condition) { + return observeSource(function replaceSource(condition) { if (!condition) { return bindTarget( observeUndefined, @@ -395,10 +420,8 @@ function makeDefinedBinder(bindTarget) { descriptor, trace ); - } else { - return Function.noop; } - }), targetScope); + }, targetScope); } } @@ -415,7 +438,7 @@ function makeParentBinder(bindTarget) { exports.makeWithBinder = makeWithBinder; function makeWithBinder(observeTarget, bindTarget) { return function bindWith(observeSource, sourceScope, targetScope, descriptor, trace) { - return observeTarget(autoCancelPrevious(function replaceTarget(target) { + return observeTarget(function replaceTarget(target) { if (target == null) { return; } @@ -426,7 +449,7 @@ function makeWithBinder(observeTarget, bindTarget) { descriptor, trace ); - }), targetScope); + }, targetScope); }; } diff --git a/observe.js b/observe.js index d46fdd6..dfc6bf8 100644 --- a/observe.js +++ b/observe.js @@ -45,9 +45,9 @@ function observe(source, expression, descriptorOrFunction) { return descriptor.change.apply(source, arguments); } else if (typeof contentChange === "function") { value.addRangeChangeListener(contentChange); - return Observers.once(function () { + return function () { value.removeRangeChangeListener(contentChange); - }); + }; } }), sourceScope); } diff --git a/observers.js b/observers.js index 09434b2..3262958 100644 --- a/observers.js +++ b/observers.js @@ -69,7 +69,7 @@ function observeProperty(object, key, emit, scope) { scope.beforeChange ); propertyChange(object[key], key, object); - return once(function cancelPropertyObserver() { + return function cancelPropertyObserver() { cancel(); PropertyChanges.removeOwnPropertyChangeListener( object, @@ -77,23 +77,23 @@ function observeProperty(object, key, emit, scope) { propertyChange, scope.beforeChange ); - }); + }; } exports.makePropertyObserver = makePropertyObserver; function makePropertyObserver(observeObject, observeKey) { return function observeProperty(emit, scope) { - return observeKey(autoCancelPrevious(function replaceKey(key) { + return observeKey(function replaceKey(key) { if (typeof key !== "string" && typeof key !== "number") return emit(); - return observeObject(autoCancelPrevious(function replaceObject(object) { + return observeObject(function replaceObject(object) { if (object == null) return emit(); if (object.observeProperty) { return object.observeProperty(key, emit, scope); } else { return _observeProperty(object, key, emit, scope); } - }), scope); - }), scope); + }, scope); + }, scope); }; } @@ -111,18 +111,18 @@ function observeGet(collection, key, emit, scope) { } mapChange(collection.get(key), key, collection); collection.addMapChangeListener(mapChange, scope.beforeChange); - return once(function cancelMapObserver() { + return function cancelMapObserver() { cancel(); collection.removeMapChangeListener(mapChange); - }); + }; } exports.makeGetObserver = makeGetObserver; function makeGetObserver(observeCollection, observeKey) { return function observeGet(emit, scope) { - return observeCollection(autoCancelPrevious(function replaceCollection(collection) { + return observeCollection(function replaceCollection(collection) { if (!collection) return emit(); - return observeKey(autoCancelPrevious(function replaceKey(key) { + return observeKey(function replaceKey(key) { if (key == null) return emit(); if (collection.observeGet) { // polymorphic override @@ -131,8 +131,8 @@ function makeGetObserver(observeCollection, observeKey) { // common case return _observeGet(collection, key, emit, scope); } - }), scope); - }), scope); + }, scope); + }, scope); }; } @@ -140,21 +140,31 @@ exports.makeHasObserver = makeHasObserver; function makeHasObserver(observeSet, observeValue) { return function observeHas(emit, scope) { emit = makeUniq(emit); - return observeValue(autoCancelPrevious(function replaceValue(sought) { - return observeSet(autoCancelPrevious(function replaceSet(set) { - if (!set) return emit(); - return observeRangeChange(set, function rangeChange() { - // this could be done incrementally if there were guarantees of - // uniqueness, but if there are guarantees of uniqueness, the - // data structure can probably efficiently check - return emit((set.has || set.contains).call(set, sought)); - }, scope); - }), scope); - }), scope); + return observeValue(function replaceValue(sought) { + return observeSet(function replaceSet(collection) { + if (!collection) { + return emit(); + } else if (collection.addRangeChangeListener) { + return observeRangeChange(collection, function rangeChange() { + // This could be done incrementally if there were + // guarantees of uniqueness, but if there are + // guarantees of uniqueness, the data structure can + // probably efficiently check + return emit((collection.has || collection.contains) + .call(collection, sought)); + }, scope); + } else if (collection.addMapChangeListener) { + return observeMapChange(collection, function mapChange() { + return emit(collection.has(sought)); + }, scope); + } else { + return emit(); + } + }, scope); + }, scope); }; } - // Compound Observers // accepts an array of observers and emits an array of the corresponding @@ -172,10 +182,10 @@ function makeObserversObserver(observers) { }, scope); }); var cancel = emit(output) || Function.noop; - return once(function cancelObserversObserver() { + return function cancelObserversObserver() { cancel(); cancelEach(cancelers); - }); + }; }; } @@ -190,9 +200,12 @@ function makeObjectObserver(observers) { var output = {}; for (var name in observers) { (function (name, observe) { + // To ensure that the property exists, even if the observer + // fails to emit: + output[name] = void 0; cancelers[name] = observe(function (value) { output[name] = value; - }, scope); + }, scope) || Function.noop; })(name, observers[name]); } var cancel = emit(output) || Function.noop; @@ -219,10 +232,10 @@ function makeOperatorObserverMaker(operator) { var observeOperands = makeObserversObserver(Array.prototype.slice.call(arguments)); var observeOperandChanges = makeRangeContentObserver(observeOperands); return function observeOperator(emit, scope) { - return observeOperandChanges(autoCancelPrevious(function (operands) { + return observeOperandChanges(function (operands) { if (!operands.every(Operators.defined)) return emit(); return emit(operator.apply(void 0, operands)); - }), scope); + }, scope); }; }; } @@ -235,29 +248,24 @@ function makeMethodObserverMaker(name) { return function makeMethodObserver(/*...observers*/) { var observeObject = arguments[0]; var operandObservers = Array.prototype.slice.call(arguments, 1); - var autoCancelingOperandObservers = operandObservers.map(function (observe) { - return function autoCancelingOperandObserver(emit, scope) { - return observe(autoCancelPrevious(emit), scope); - }; - }); var observeOperands = makeObserversObserver(operandObservers); var observeOperandChanges = makeRangeContentObserver(observeOperands); return function observeMethod(emit, scope) { - return observeObject(autoCancelPrevious(function (object) { + return observeObject(function (object) { if (!object) return emit(); if (object[makeObserverName]) - return object[makeObserverName].apply(object, autoCancelingOperandObservers)(emit, scope); + return object[makeObserverName].apply(object, operandObservers)(emit, scope); if (object[observeName]) return object[observeName](emit, scope); - return observeOperandChanges(autoCancelPrevious(function (operands) { + return observeOperandChanges(function (operands) { if (!operands.every(Operators.defined)) return emit(); if (typeof object[name] === "function") { return emit(object[name].apply(object, operands)); } else { return emit(); } - }), scope); - }), scope); + }, scope); + }, scope); }; }; } @@ -268,9 +276,9 @@ function makeMethodObserverMaker(name) { exports.makeNotObserver = makeNotObserver; function makeNotObserver(observeValue) { return function observeNot(emit, scope) { - return observeValue(autoCancelPrevious(function replaceValue(value) { + return observeValue(function replaceValue(value) { return emit(!value); - }), scope); + }, scope); }; } @@ -280,26 +288,26 @@ function makeNotObserver(observeValue) { exports.makeAndObserver = makeAndObserver; function makeAndObserver(observeLeft, observeRight) { return function observeAnd(emit, scope) { - return observeLeft(autoCancelPrevious(function replaceLeft(left) { + return observeLeft(function replaceLeft(left) { if (!left) { return emit(left); } else { return observeRight(emit, scope); } - }), scope); + }, scope); }; } exports.makeOrObserver = makeOrObserver; function makeOrObserver(observeLeft, observeRight) { return function observeOr(emit, scope) { - return observeLeft(autoCancelPrevious(function replaceLeft(left) { + return observeLeft(function replaceLeft(left) { if (left) { return emit(left); } else { return observeRight(emit, scope); } - }), scope); + }, scope); }; } @@ -308,7 +316,7 @@ function makeOrObserver(observeLeft, observeRight) { exports.makeConditionalObserver = makeConditionalObserver; function makeConditionalObserver(observeCondition, observeConsequent, observeAlternate) { return function observeConditional(emit, scope) { - return observeCondition(autoCancelPrevious(function replaceCondition(condition) { + return observeCondition(function replaceCondition(condition) { if (condition == null) { return emit(); } else if (condition) { @@ -316,7 +324,7 @@ function makeConditionalObserver(observeCondition, observeConsequent, observeAlt } else { return observeAlternate(emit, scope); } - }), scope); + }, scope); }; } @@ -325,22 +333,22 @@ function makeConditionalObserver(observeCondition, observeConsequent, observeAlt exports.makeDefinedObserver = makeDefinedObserver; function makeDefinedObserver(observeValue) { return function observeDefault(emit, scope) { - return observeValue(autoCancelPrevious(function replaceValue(value) { + return observeValue(function replaceValue(value) { return emit(value != null); - }), scope); + }, scope); }; } exports.makeDefaultObserver = makeDefaultObserver; function makeDefaultObserver(observeValue, observeAlternate) { return function observeDefault(emit, scope) { - return observeValue(autoCancelPrevious(function replaceValue(value) { + return observeValue(function replaceValue(value) { if (value == null) { return observeAlternate(emit, scope); } else { return emit(value); } - }), scope); + }, scope); }; } @@ -354,7 +362,7 @@ function makeDefaultObserver(observeValue, observeAlternate) { var makeMapBlockObserver = exports.makeMapBlockObserver = makeNonReplacing(makeReplacingMapBlockObserver); function makeReplacingMapBlockObserver(observeCollection, observeRelation) { return function observeMap(emit, scope) { - return observeCollection(autoCancelPrevious(function replaceMapInput(input) { + return observeCollection(function replaceMapInput(input) { if (!input) return emit(); var output = []; @@ -376,7 +384,7 @@ function makeReplacingMapBlockObserver(observeCollection, observeRelation) { var initial = []; cancelEach(cancelers.swap(index, minus.length, plus.map(function (value, offset) { var indexRef = indexRefs[index + offset]; - return observeRelation(autoCancelPrevious(function replaceRelationOutput(value) { + return observeRelation(function replaceRelationOutput(value) { if (initialized) { output.set(indexRef.index, value); } else { @@ -384,7 +392,7 @@ function makeReplacingMapBlockObserver(observeCollection, observeRelation) { // does not dispatch changes. initial[offset] = value; } - }), scope.nest(value)); + }, scope.nest(value)); }))); initialized = true; output.swap(index, minus.length, initial); @@ -395,12 +403,12 @@ function makeReplacingMapBlockObserver(observeCollection, observeRelation) { // mapping observer, utilized by filter observers var cancel = emit(output, input) || Function.noop; - return once(function cancelMapObserver() { + return function cancelMapObserver() { cancel(); cancelEach(cancelers); cancelRangeChange(); - }); - }), scope); + }; + }, scope); }; } @@ -408,7 +416,7 @@ var makeFilterBlockObserver = exports.makeFilterBlockObserver = makeNonReplacing function makeReplacingFilterBlockObserver(observeCollection, observePredicate) { var observePredicates = makeReplacingMapBlockObserver(observeCollection, observePredicate); return function observeFilter(emit, scope) { - return observePredicates(autoCancelPrevious(function (predicates, input) { + return observePredicates(function (predicates, input) { if (!input) return emit(); var output = []; @@ -444,13 +452,13 @@ function makeReplacingFilterBlockObserver(observeCollection, observePredicate) { var cancelRangeChange = observeRangeChange(predicates, rangeChange, scope); var cancel = emit(output) || Function.noop; - return once(function cancelFilterObserver() { + return function cancelFilterObserver() { cancel(); cancelEach(cancelers); cancelRangeChange(); - }); + }; - }), scope); + }, scope); }; } @@ -463,7 +471,7 @@ function makeSortedBlockObserver(observeCollection, observeRelation) { // produces: map{[this, key]) var observeRelationEntries = makeReplacingMapBlockObserver(observeCollection, observeRelationEntry); var observeSort = function (emit, scope) { - return observeRelationEntries(autoCancelPrevious(function (input) { + return observeRelationEntries(function (input) { // [[value, relatedValue], ...] if (!input) return emit(); var output = []; @@ -478,7 +486,7 @@ function makeSortedBlockObserver(observeCollection, observeRelation) { cancel(); cancelRangeChange(); }; - }), scope); + }, scope); }; return makeMapBlockObserver(observeSort, observeEntryKey); } @@ -494,9 +502,9 @@ function entryValueCompare(x, y) { // Transforms a value into a [value, relation(value)] tuple function makeRelationEntryObserver(observeRelation) { return function observeRelationEntry(emit, scope) { - return observeRelation(autoCancelPrevious(function replaceRelation(value) { + return observeRelation(function replaceRelation(value) { return emit([scope.value, value]); - }), scope); + }, scope); }; } @@ -549,7 +557,7 @@ function makeSortedSetBlockObserver(observeCollection, observeRelation) { exports.makeReversedObserver = makeNonReplacing(makeReplacingReversedObserver); function makeReplacingReversedObserver(observeArray) { return function observeReversed(emit, scope) { - return observeArray(autoCancelPrevious(function (input) { + return observeArray(function (input) { if (!input) return emit(); var output = []; function rangeChange(plus, minus, index) { @@ -558,11 +566,11 @@ function makeReplacingReversedObserver(observeArray) { } var cancelRangeChange = observeRangeChange(input, rangeChange, scope); var cancel = emit(output); - return once(function cancelReversedObserver() { + return function cancelReversedObserver() { cancel(); cancelRangeChange(); - }); - }), scope); + }; + }, scope); }; } @@ -570,7 +578,7 @@ var makeFlattenObserver = exports.makeFlattenObserver = makeNonReplacing(makeReplacingFlattenObserver); function makeReplacingFlattenObserver(observeArray) { return function (emit, scope) { - return observeArray(autoCancelPrevious(function (input) { + return observeArray(function (input) { if (!input) return emit(); var output = []; @@ -624,12 +632,12 @@ function makeReplacingFlattenObserver(observeArray) { var cancelRangeChange = observeRangeChange(input, rangeChange, scope); var cancel = emit(output) || Function.noop; - return once(function cancelFlattenObserver() { + return function cancelFlattenObserver() { cancel(); cancelEach(cancelers); cancelRangeChange(); - }); - }), scope); + }; + }, scope); }; } @@ -675,7 +683,7 @@ function makeGroupMapBlockObserver(observeCollection, observeRelation) { var observeRelationEntry = makeRelationEntryObserver(observeRelation); var observeRelationEntries = makeReplacingMapBlockObserver(observeCollection, observeRelationEntry); return function observeGroup(emit, scope) { - return observeRelationEntries(autoCancelPrevious(function (input, original) { + return observeRelationEntries(function (input, original) { if (!input) return emit(); var groups = Map(); @@ -714,7 +722,7 @@ function makeGroupMapBlockObserver(observeCollection, observeRelation) { cancelRangeChange(); cancel(); }; - }), scope); + }, scope); }; } @@ -728,7 +736,7 @@ function makeHeapBlockObserver(observeCollection, observeRelation, order) { return function observeHeapBlock(emit, scope) { - return observeRelationEntries(autoCancelPrevious(function (input) { + return observeRelationEntries(function (input) { if (!input) return emit(); var heap = new Heap(null, entryValueEquals, entryValueOrderCompare); @@ -755,7 +763,7 @@ function makeHeapBlockObserver(observeCollection, observeRelation, order) { cancelRangeChange(); cancelHeapChange(); }; - }), scope); + }, scope); }; } @@ -790,13 +798,13 @@ function makeCollectionObserverMaker(setup) { return function (observeCollection) { return function (emit, scope) { emit = makeUniq(emit); - return observeCollection(autoCancelPrevious(function (collection) { + return observeCollection(function (collection) { if (!collection) return emit(); var rangeChange = setup(collection, emit); return observeRangeChange(collection, function (plus, minus, index) { return emit(rangeChange(plus, minus, index)); }, scope); - }), scope); + }, scope); }; }; } @@ -835,7 +843,7 @@ function makeReplacingViewObserver(observeInput, observeStart, observeLength) { observeStart = observeZero; } return function observeView(emit, scope) { - return observeInput(autoCancelPrevious(function (input) { + return observeInput(function (input) { if (!input) return emit(); var output = []; @@ -971,7 +979,7 @@ function makeReplacingViewObserver(observeInput, observeStart, observeLength) { cancelStartObserver(); cancelRangeChangeObserver(); }; - }), scope); + }, scope); }; } @@ -981,7 +989,7 @@ exports.makeEnumerateObserver = makeNonReplacing(makeReplacingEnumerateObserver) exports.makeEnumerationObserver = exports.makeEnumerateObserver; // deprecated function makeReplacingEnumerateObserver(observeArray) { return function (emit, scope) { - return observeArray(autoCancelPrevious(function replaceArray(input) { + return observeArray(function replaceArray(input) { if (!input) return emit(); var output = []; @@ -1002,7 +1010,7 @@ function makeReplacingEnumerateObserver(observeArray) { cancel(); cancelRangeChange(); }; - }), scope); + }, scope); }; } @@ -1078,9 +1086,9 @@ exports.makeJoinObserver = makeJoinObserver; function makeJoinObserver(observeArray, observeDelimiter) { observeDelimiter = observeDelimiter || observeNullString; return function observeJoin(emit, scope) { - return observeArray(autoCancelPrevious(function changeJoinArray(array) { + return observeArray(function changeJoinArray(array) { if (!array) return emit(); - return observeDelimiter(autoCancelPrevious(function changeJoinDelimiter(delimiter) { + return observeDelimiter(function changeJoinDelimiter(delimiter) { if (typeof delimiter !== "string") return emit(); var cancel = Function.noop; function rangeChange() { @@ -1091,8 +1099,8 @@ function makeJoinObserver(observeArray, observeDelimiter) { cancelRangeChange(); cancel(); }; - }), scope); - }), scope); + }, scope); + }, scope); }; } @@ -1132,9 +1140,9 @@ function observeRangeChange(collection, emit, scope) { exports.makeLastObserver = makeLastObserver; function makeLastObserver(observeCollection) { return function observeLast(emit, scope) { - return observeCollection(autoCancelPrevious(function (collection) { + return observeCollection(function (collection) { return _observeLast(collection, emit, scope); - }), scope); + }, scope); }; } @@ -1173,9 +1181,9 @@ function observeLast(collection, emit, scope) { exports.makeOnlyObserver = makeOnlyObserver; function makeOnlyObserver(observeCollection) { return function (emit, scope) { - return observeCollection(autoCancelPrevious(makeUniq(function replaceCollectionForOnly(collection) { + return observeCollection(makeUniq(function replaceCollectionForOnly(collection) { return observeOnly(collection, emit, scope); - })), scope); + }), scope); }; } @@ -1195,9 +1203,9 @@ function observeOnly(collection, emit, scope) { exports.makeOneObserver = makeOneObserver; function makeOneObserver(observeCollection) { return function (emit, scope) { - return observeCollection(autoCancelPrevious(makeUniq(function replaceCollectionForOne(collection) { + return observeCollection(makeUniq(function replaceCollectionForOne(collection) { return observeOne(collection, emit, scope); - })), scope); + }), scope); }; } @@ -1218,7 +1226,7 @@ function observeOne(collection, emit, scope) { exports.makeRangeContentObserver = makeRangeContentObserver; function makeRangeContentObserver(observeCollection) { return function observeContent(emit, scope) { - return observeCollection(autoCancelPrevious(function (collection) { + return observeCollection(function (collection) { if (!collection || !collection.addRangeChangeListener) { return emit(collection); } else { @@ -1226,14 +1234,14 @@ function makeRangeContentObserver(observeCollection) { return emit(collection); }, scope); } - }), scope); + }, scope); }; } exports.makeMapContentObserver = makeMapContentObserver; function makeMapContentObserver(observeCollection) { return function observeContent(emit, scope) { - return observeCollection(autoCancelPrevious(function (collection) { + return observeCollection(function (collection) { if (!collection || !collection.addMapChangeListener) { return emit(collection); } else { @@ -1241,7 +1249,7 @@ function makeMapContentObserver(observeCollection) { return emit(collection); }, scope); } - }), scope); + }, scope); }; } @@ -1263,21 +1271,21 @@ function observeMapChange(collection, emit, scope) { } collection.forEach(mapChange); var cancelMapChange = collection.addMapChangeListener(mapChange, scope.beforeChange); - return once(function cancelMapObserver() { + return function cancelMapObserver() { cancelers.forEach(function (cancel) { cancel(); }); cancelMapChange(); - }); + }; } var makeEntriesObserver = exports.makeEntriesObserver = makeNonReplacing(makeReplacingEntriesObserver); function makeReplacingEntriesObserver(observeCollection) { return function _observeEntries(emit, scope) { - return observeCollection(autoCancelPrevious(function (collection) { + return observeCollection(function (collection) { if (!collection) return emit(); return observeEntries(collection, emit, scope); - }), scope); + }, scope); }; } @@ -1304,10 +1312,10 @@ function observeEntries(collection, emit, scope) { } } var cancelMapChange = observeMapChange(collection, mapChange, scope) || Function.noop; - return once(function cancelObserveEntries() { + return function cancelObserveEntries() { cancel(); cancelMapChange(); - }); + }; } exports.makeKeysObserver = makeKeysObserver; @@ -1340,14 +1348,14 @@ function makeToMapObserver(observeObject) { var map = new Map(); var cancel = emit(map) || Function.noop; - var cancelObjectObserver = observeObject(autoCancelPrevious(function replaceObject(object) { + var cancelObjectObserver = observeObject(function replaceObject(object) { map.clear(); if (!object || typeof object !== "object") return; // Must come first because Arrays also implement map changes, but // Maps do not implement range changes. if (object.addRangeChangeListener) { // array/collection of items - return observeUniqueEntries(autoCancelPrevious(function (entries) { + return observeUniqueEntries(function (entries) { function rangeChange(plus, minus) { minus.forEach(function (entry) { map["delete"](entry[0]); @@ -1357,7 +1365,7 @@ function makeToMapObserver(observeObject) { }); } return observeRangeChange(entries, rangeChange, scope); - }), scope.nest(object)); + }, scope.nest(object)); } else if (object.addMapChangeListener) { // map reflection return observeMapChange(object, function mapChange(value, key) { if (value === undefined) { @@ -1368,19 +1376,19 @@ function makeToMapObserver(observeObject) { }, scope); } else { // object literal var cancelers = Object.keys(object).map(function (key) { - return _observeProperty(object, key, autoCancelPrevious(function (value) { + return _observeProperty(object, key, function (value) { if (value === undefined) { map["delete"](key); } else { map.set(key, value); } - }), scope); + }, scope); }); return function cancelPropertyObservers() { cancelEach(cancelers); }; } - }), scope); + }, scope) || Function.noop; return function cancelObjectToMapObserver() { cancel(); @@ -1417,9 +1425,9 @@ exports.makeConverterObserver = makeConverterObserver; function makeConverterObserver(observeValue, convert, thisp) { return function observeConversion(emit, scope) { emit = makeUniq(emit); - return observeValue(autoCancelPrevious(function replaceValue(value) { + return observeValue(function replaceValue(value) { return emit(convert.call(thisp, value)); - }), scope); + }, scope); }; } @@ -1427,10 +1435,10 @@ exports.makeComputerObserver = makeComputerObserver; function makeComputerObserver(observeArgs, compute, thisp) { return function (emit, scope) { emit = makeUniq(emit); - return observeArgs(autoCancelPrevious(function replaceArgs(args) { + return observeArgs(function replaceArgs(args) { if (!args || !args.every(Operators.defined)) return; return emit(compute.apply(thisp, args)); - }), scope); + }, scope); }; } @@ -1441,7 +1449,7 @@ function makeExpressionObserver(observeInput, observeExpression) { var compileObserver = require("./compile-observer"); return function expressionObserver(emit, scope) { emit = makeUniq(emit); - return observeExpression(autoCancelPrevious(function replaceExpression(expression) { + return observeExpression(function replaceExpression(expression) { if (expression == null) return emit(); var syntax, observeOutput; try { @@ -1450,10 +1458,10 @@ function makeExpressionObserver(observeInput, observeExpression) { } catch (exception) { return emit(); } - return observeInput(autoCancelPrevious(function replaceInput(input) { + return observeInput(function replaceInput(input) { return observeOutput(emit, scope.nest(input)); - }), scope); - }), scope); + }, scope); + }, scope); }; } @@ -1463,13 +1471,13 @@ function makeExpressionObserver(observeInput, observeExpression) { exports.makeWithObserver = makeWithObserver; function makeWithObserver(observeInput, observeExpression) { return function observeWith(emit, scope) { - return observeInput(autoCancelPrevious(function replaceInput(input) { + return observeInput(function replaceInput(input) { if (input == null) return emit(); - return observeExpression(autoCancelPrevious(function replaceValue(value) { + return observeExpression(function replaceValue(value) { if (value == null) return emit(); return emit(value); - }), scope.nest(input)); - }), scope); + }, scope.nest(input)); + }, scope); }; } @@ -1490,7 +1498,7 @@ function makeNonReplacing(wrapped) { var observe = wrapped.apply(this, arguments); return function observeArrayWithoutReplacing(emit, scope) { var output = []; - var cancelObserver = observe(autoCancelPrevious(function (input) { + var cancelObserver = observe(function (input) { function rangeChange(plus, minus, index) { output.swap(index, minus.length, plus); } @@ -1505,14 +1513,14 @@ function makeNonReplacing(wrapped) { null, scope.beforeChange ); - return once(cancelRangeChange); + return cancelRangeChange; } - }), scope); + }, scope); var cancel = emit(output) || Function.noop; - return once(function cancelNonReplacingObserver() { + return function cancelNonReplacingObserver() { cancelObserver(); cancel(); - }); + }; }; }; } @@ -1539,34 +1547,16 @@ function cancelEach(cancelers) { }); } -// wraps an emitter that returns a canceler. each time the wrapped function is -// called, it cancels the previous canceler, and calls the last canceler when -// it is canceled. this is useful for observers that update a value and attach -// a new event listener tree to the value. +// This emit function decorator originally ensured that the previous canceller +// returned by emit would be called before dispatching emit again. This was +// superfluous and its removal did not incur any noticable memory leakage or +// correctness issues. In fact, the behavior of equality binders mysteriously +// improved. +// XXX deprecated Retained because this function is used in Montage. exports.autoCancelPrevious = autoCancelPrevious; function autoCancelPrevious(emit) { - var cancelPrevious = Function.noop; - return function replaceObserver(value) { - cancelPrevious(); - cancelPrevious = emit.apply(this, arguments) || Function.noop; - return function cancelObserver() { - cancelPrevious(); - cancelPrevious = Function.noop; - }; - }; -} - -exports.once = once; -function once(callback) { - var done; - return function once() { - if (done) { - return Function.noop; // TODO fix bugs that make this sensitive - //throw new Error("Redundant call: " + callback + " " + done.stack + "\nSecond call:"); - } - done = true; - //done = new Error("First call:"); - return callback.apply(this, arguments); + return function () { + return emit.apply(null, arguments) || Function.noop; }; } diff --git a/package.json b/package.json index fdca15d..f68ca51 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ }, "devDependencies": { "jasmine-node": "1.x.x", - "pegjs": "git://github.com/dmajda/pegjs.git" + "pegjs": "git://github.com/dmajda/pegjs.git", + "memwatch": "*" }, "scripts": { "test": "jasmine-node spec", diff --git a/spec/bind-spec.js b/spec/bind-spec.js index 724a1af..c66a81c 100644 --- a/spec/bind-spec.js +++ b/spec/bind-spec.js @@ -8,12 +8,18 @@ Error.stackTraceLimit = 100; describe("bind", function () { describe("<-", function () { - var source = {foo: {bar: {baz: 10}}}; - var target = {foo: {bar: {baz: undefined}}}; - var cancel = bind(target, "foo.bar.baz", { - "<-": "foo.bar.baz", - "source": source + var source, target, cancel; + + it("set up", function () { + source = {foo: {bar: {baz: 10}}}; + target = {foo: {bar: {baz: undefined}}}; + + cancel = bind(target, "foo.bar.baz", { + "<-": "foo.bar.baz", + "source": source + }); + }); it("initial", function () { @@ -25,10 +31,13 @@ describe("bind", function () { describe("<->", function () { - var object = {bar: 10}; - object.self = object; + var object, cancel; - var cancel = bind(object, "self.foo", {"<->": "self.bar"}); + it("set up", function () { + object = {bar: 10}; + object.self = object; + cancel = bind(object, "self.foo", {"<->": "self.bar"}); + }); it("initial", function () { expect(object.foo).toBe(10); @@ -60,7 +69,7 @@ describe("bind", function () { }); - describe("sum", function () { + it("sum", function () { var object = {values: [1,2,3]}; var cancel = bind(object, "sum", {"<-": "values.sum{}"}); expect(object.sum).toBe(6); @@ -71,7 +80,7 @@ describe("bind", function () { expect(object.sum).toBe(10); }); - describe("average", function () { + it("average", function () { var object = {values: [1,2,3]}; var cancel = bind(object, "average", {"<-": "values.average{}"}); expect(object.average).toBe(2); @@ -82,7 +91,7 @@ describe("bind", function () { expect(object.average).toBe(2.5); }); - describe("content", function () { + it("content", function () { var foo = [1, 2, 3]; var bar = []; var object = {foo: foo, bar: bar}; @@ -96,7 +105,7 @@ describe("bind", function () { expect(object.bar).toBe(bar); }); - describe("reversed", function () { + it("reversed", function () { var object = {foo: [1,2,3]}; var cancel = bind(object, "bar", {"<-": "foo.reversed{}"}); expect(object.bar).toEqual([3, 2, 1]); @@ -109,7 +118,7 @@ describe("bind", function () { expect(object.bar).toEqual([4, 3, 'c', 'b', 'a', 2, 1]); }); - describe("reversed left hand side", function () { + it("reversed left hand side", function () { var object = {foo: [1,2,3]}; var cancel = bind(object, "bar", {"<->": "foo.reversed()"}); // object.bar has to be sliced since observable arrays are not @@ -128,7 +137,7 @@ describe("bind", function () { expect(object.bar.slice()).toEqual([4, 3, 'c', 'b', 'a', 2]); }); - describe("tuple", function () { + it("tuple", function () { var object = {a: 10, b: 20, c: 30}; var cancel = bind(object, "d", {"<-": "[a, b, c]"}); expect(object.d).toEqual([10, 20, 30]); @@ -137,7 +146,7 @@ describe("bind", function () { expect(object.d).toEqual([10, 20, 30]); }); - describe("record", function () { + it("record", function () { var object = {foo: 10, bar: 20}; var cancel = bind(object, "record", {"<-": "{a: foo, b: bar}"}); expect(object.record).toEqual({a: 10, b: 20}); @@ -148,7 +157,7 @@ describe("bind", function () { expect(object.record).toEqual({a: 20, b: 20}); }); - describe("record map", function () { + it("record map", function () { var object = {arrays: [[1, 2, 3], [4, 5, 6]]}; var cancel = bind(object, "summaries", { "<-": "arrays.map{{length: length, sum: sum()}}" @@ -167,13 +176,13 @@ describe("bind", function () { ]); }); - describe("literals", function () { + it("literals", function () { var object = {}; var cancel = bind(object, "literals", {"<-": "[0, 'foo bar']"}); expect(object.literals).toEqual([0, "foo bar"]); }); - describe("has", function () { + it("has", function () { var object = {set: [1, 2, 3], sought: 2}; var cancel = bind(object, "has", {"<-": "set.has(sought)"}); @@ -198,7 +207,7 @@ describe("bind", function () { expect(object.set.slice()).toEqual([1, 2, 3, 4]); }); - describe("has <-", function () { + it("has <-", function () { var object = {set: [1, 2, 3], sought: 2}; var cancel = bind(object, "has", {"<->": "set.has(sought)"}); @@ -211,7 +220,7 @@ describe("bind", function () { }); - describe("map", function () { + it("map", function () { var object = { foo: [{bar: 10}, {bar: 20}, {bar: 30}] }; @@ -223,7 +232,7 @@ describe("bind", function () { expect(object.baz).toEqual([10, 20, 30, 40]); }); - describe("filter", function () { + it("filter", function () { var object = { foo: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }; @@ -233,7 +242,7 @@ describe("bind", function () { expect(object.bar).toEqual([2, 4, 6, 8, 10]); }); - describe("flatten", function () { + it("flatten", function () { var object = { foo: [[1], [2, 3], [4]] }; @@ -260,7 +269,7 @@ describe("bind", function () { expect(object.baz).toEqual([0, 1, 4, 5, 6]); }); - describe("flatten map", function () { + it("flatten map", function () { var object = { foo: [{bar: [1]}, {bar: [2, 3]}, {bar: [4]}] }; @@ -287,7 +296,7 @@ describe("bind", function () { expect(object.baz).toEqual([0, 1, 4, 5, 6]); }); - describe("tree replacement", function () { + it("tree replacement", function () { var object = {qux: 10, foo: {bar: {baz: null}}}; var cancel = bind(object, "foo.bar.baz", {"<->": "qux"}); expect(object.foo.bar.baz).toEqual(10); @@ -300,7 +309,7 @@ describe("bind", function () { expect(object.qux).toEqual(30); }); - describe("parameters", function () { + it("parameters", function () { var object = {}; var parameters = {a: 10, b: 20, c: 30}; var source = [1, 2, 3]; @@ -316,22 +325,27 @@ describe("bind", function () { expect(object.foo).toEqual([0, 20, [30, 30, 30, 30]]); }); - describe("equality and addition", function () { + it("equality and addition", function () { var object = {a: 2, b: 1, c: 1}; var cancel = bind(object, "d", {"<->": "a == b + c"}); + // 2 == 1 + 1 expect(object.d).toEqual(true); object.a = 3; + // 3 == 1 + 1 expect(object.d).toEqual(false); object.b = 2; + // 3 == 2 + 1 expect(object.d).toEqual(true); object.c = 2; + // 3 == 2 + 2 expect(object.d).toEqual(false); - expect(object.a).toEqual(4); + expect(object.a).toEqual(3); object.d = true; + // 4 == 2 + 2 expect(object.a).toEqual(4); }); - describe("two-way negation", function () { + it("two-way negation", function () { var object = {}; bind(object, "a", {"<->": "!b"}); @@ -351,25 +365,29 @@ describe("bind", function () { expect(object.b).toBe(false); }); - describe("equality and assignment", function () { + it("equality and assignment", function () { var object = {choice: 2, a: 2, b: 3}; bind(object, "isA", {"<->": "!isB"}); bind(object, "choice == a", {"<->": "isA"}); bind(object, "choice == b", {"<->": "isB"}); + // choice: 2, a: 2, b: 3, isA: true, isB: false expect(object.choice).toBe(2); object.isB = true; + // choice: 3, a: 2, b: 3, isA: false, isB: true expect(object.isB).toBe(true); expect(object.isA).toBe(false); expect(object.choice).toBe(3); object.b = 4; - expect(object.isB).toBe(true); - expect(object.isA).toBe(false); - expect(object.choice).toBe(4); + // choice: 2, a: 2, b: 3, isA: true, isB: false + expect(object.isB).toBe(false); + expect(object.isA).toBe(true); + expect(object.choice).toBe(2); object.isB = true; + // isB: true, isA: false, choice: 4 expect(object.choice).toBe(4); object.isA = true; @@ -380,7 +398,7 @@ describe("bind", function () { expect(object.choice).toBe(4); }); - describe("gt", function () { + it("gt", function () { var object = {a: 1, b: 2}; bind(object, "gt", {"<-": "a > b"}); expect(object.gt).toBe(false); @@ -388,13 +406,13 @@ describe("bind", function () { expect(object.gt).toBe(true); }); - describe("algebra", function () { + it("algebra", function () { var object = {}; bind(object, "result", {"<-": "2 ** 3 * 3 + 7"}); expect(object.result).toBe(Math.pow(2, 3) * 3 + 7); }); - describe("logic", function () { + it("logic", function () { var object = {a: false, b: false}; bind(object, "result", {"<-": "a || b"}); expect(object.result).toBe(false); @@ -407,7 +425,7 @@ describe("bind", function () { expect(object.result).toBe(false); }); - describe("convert, revert", function () { + it("convert, revert", function () { var object = {a: 10}; var cancel = bind(object, "b", { "<->": "a", @@ -426,7 +444,7 @@ describe("bind", function () { expect(object.b).toEqual(12); }); - describe("add <-> sub", function () { + it("add <-> sub", function () { var object = {a: 10}; var cancel = bind(object, "b", { "<->": "a + 1" @@ -439,7 +457,7 @@ describe("bind", function () { expect(object.b).toEqual(12); }); - describe("pow <-> log", function () { + it("pow <-> log", function () { var object = {a: 2, b: 3}; var cancel = bind(object, "c", { "<->": "a ** b" @@ -451,7 +469,7 @@ describe("bind", function () { expect(object.c).toEqual(9); }); - describe("converter", function () { + it("converter", function () { var object = {a: 10}; var cancel = bind(object, "b", { "<->": "a", @@ -472,7 +490,7 @@ describe("bind", function () { expect(object.b).toEqual(12); }); - describe("content binding from sorted set", function () { + it("content binding from sorted set", function () { var array = ['a', 'c', 'b']; var set = SortedSet([4, 5, 1, 3, 45, 1, 8]); var cancel = bind(array, "rangeContent()", {"<-": "", source: set}); @@ -483,7 +501,7 @@ describe("bind", function () { expect(array.slice()).toEqual([1, 2, 3, 4, 5, 8]); }); - describe("view of a array", function () { + it("view of a array", function () { var source = { content: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], index: 2, @@ -505,7 +523,7 @@ describe("bind", function () { expect(target.slice()).toEqual([3, 4]); }); - describe("view of a sorted set", function () { + it("view of a sorted set", function () { var array = ['a', 'c', 'b']; var set = SortedSet([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); var source = { diff --git a/spec/readme-spec.js b/spec/readme-spec.js index 3c13360..6c2ce2b 100644 --- a/spec/readme-spec.js +++ b/spec/readme-spec.js @@ -87,6 +87,12 @@ describe("Tutorial", function () { expect(a.b).toBe(30); // from before it was orphaned }); + it("Strings", function () { + var object = {name: "world"}; + bind(object, "greeting", {"<-": "'hello ' + name + '!'"}); + expect(object.greeting).toBe("hello world!"); + }); + it("Sum", function () { var object = {array: [1, 2, 3]}; bind(object, "sum", {"<-": "array.sum()"}); @@ -557,6 +563,14 @@ describe("Tutorial", function () { var Set = require("collections/set"); object.haystack = new Set([1, 2, 3]); expect(object.hasNeedle).toBe(true); + + // Continued from above... + var Map = require("collections/map"); + object.haystack = new Map([[1, "a"], [2, "b"]]); + object.needle = 2; + expect(object.hasNeedle).toBe(true); + object.needle = 3; + expect(object.hasNeedle).toBe(false); }); it("Has (DOM)", function () { @@ -653,18 +667,18 @@ describe("Tutorial", function () { var object = Bindings.defineBindings({}, { keys: {"<-": "map.keys()"}, values: {"<-": "map.values()"}, - items: {"<-": "map.items()"} + entries: {"<-": "map.entries()"} }); object.map = Map({a: 10, b: 20, c: 30}); expect(object.keys).toEqual(['a', 'b', 'c']); expect(object.values).toEqual([10, 20, 30]); - expect(object.items).toEqual([['a', 10], ['b', 20], ['c', 30]]); + expect(object.entries).toEqual([['a', 10], ['b', 20], ['c', 30]]); object.map.set('d', 40); object.map.delete('a'); expect(object.keys).toEqual(['b', 'c', 'd']); expect(object.values).toEqual([20, 30, 40]); - expect(object.items).toEqual([['b', 20], ['c', 30], ['d', 40]]); + expect(object.entries).toEqual([['b', 20], ['c', 30], ['d', 40]]); }); it("Coerce to Map", function () {