diff --git a/CHANGES.md b/CHANGES.md index d1842ab..1de2bea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,10 @@ +# v0.2.18 + +- Add support for `slice(start, end)` bindings on arrays. Strings worked + previously as a consequence of method dispatch. Arrays now work, but with + caveats. + # v0.2.17 - Fix evaluator for ternary conditional operator to match the behavior of the diff --git a/README.md b/README.md index e20d2f9..1afba7d 100644 --- a/README.md +++ b/README.md @@ -724,6 +724,86 @@ controller.index.add(0); expect(controller.view).toEqual([5, 6, 7]); ``` +Because `view(start, length)` is optimized by FRB to make minimal changes to the +output array when `start` or `length` change, in most cases it is better than a +hypothetical `slice(start, start + length)` operator. There is no `slice` +operator for arrays in FRB. + +### Slice + +**Reactive support for arrays added in version 0.2.18** + +The slice operator operates on both strings and arrays and produces a window +from a `start` offset up to but not including an `end` offset. +This operator is unusual in FRB because it operates on either arrays and +strings. + +```javascript +var object = Bindings.defineBindings({ + start: 2, + end: 4 +}, { + slice: {"<-": "cake.slice(start, end)"} +}); + +object.cake = "abcdefg"; +expect(object.slice).toBe("cd"); + +object.cake = [1, 2, 3, 4, 5, 6, 7]; +expect(object.slice).toEqual([3, 4]); +``` + +The behavior for strings is unimaginative. +If the source string, or either the start index or end index changes, the target +will be replaced with that slice of the source. + +The behavior for arrays is nuanced. +To avoid these nuances, you should generally use the `view(start, length)` +operator directly. +It is better suited for viewing a sliding window of an array, allowing +the position and size of the window to change orthogonally. + +However, if you happen to use the `slice` operator on an array, you can expect +it to deviate from the norm. +Most FRB operators that produce arrays will only emit that array once, even if +the value of the input changes. +This is beneficial because one can attach change listeners once to an output +array knowing that these change listeners will continue to react until the +binding is canceled, even if the input temporarily null or is replaced. +Because the `slice` operator may have either a string or an array as its input, +the output value will change if ever the input value is replaced and may switch +between being an array or string depending on the input type. + +Altering the content of the source does not change the value of the output. + +```javascript +// Continued from above... +var original = object.slice; +object.cake.shift(); +expect(object.slice).toEqual([4, 5]); +expect(object.slice).toBe(original); +``` + +Altering the `start` or `end` parameters will also only change the content of +the output. + +```javascript +// Continued from above... +object.start--; +object.end++; +expect(object.cake).toEqual([2, 3, 4, 5, 6, 7]); +expect(object.slice).toEqual([3, 4, 5, 6]); +expect(object.slice).toBe(original); +``` + +But replacing the input will produce a new output value. +```javascript +// Continued from above... +object.cake = [7, 6, 5, 4, 3, 2, 1, 0]; +expect(object.slice).toEqual([6, 5, 4, 3]); +expect(object.slice).not.toBe(original); +``` + ### Enumerate An enumeration observer produces `[index, value]` pairs. You can bind @@ -2108,6 +2188,7 @@ Bindings.defineBindings({ }, { b: { "<-": "a + 1", + trace: true } }); ``` diff --git a/bind.js b/bind.js index 7b785fc..4583c2c 100644 --- a/bind.js +++ b/bind.js @@ -203,7 +203,7 @@ function bindRangeContent( source.swap(0, source.length, target); isActive = false; cancel = establishRangeContentBinding(); - } else if (!source && !isActive) { + } else if (!source && !isActive && target.clone) { trace && console.log("RANGE CONTENT TARGET INITIALIZED TO COPY OF SOURCE", trace.targetPath, "<-", tarce.sourcePath, "WITH", source); assignSource(target.clone(), sourceScope); } @@ -223,7 +223,7 @@ function bindRangeContent( target.swap(0, target.length, source); isActive = false; cancel = establishRangeContentBinding(); - } else if (!target) { + } else if (!target && source.clone) { assignTarget(source.clone(), targetScope); } } diff --git a/compile-observer.js b/compile-observer.js index 08ecd6d..85189bb 100644 --- a/compile-observer.js +++ b/compile-observer.js @@ -45,6 +45,7 @@ var semantics = compile.semantics = { flatten: Observers.makeFlattenObserver, concat: Observers.makeConcatObserver, view: Observers.makeViewObserver, + slice: Observers.makeSliceObserver, sum: Observers.makeSumObserver, average: Observers.makeAverageObserver, last: Observers.makeLastObserver, diff --git a/observers.js b/observers.js index 2c09e62..2ca8bc2 100644 --- a/observers.js +++ b/observers.js @@ -971,6 +971,27 @@ function makeReplacingViewObserver(observeInput, observeStart, observeLength) { }; } +exports.makeSliceObserver = makeSliceObserver; +function makeSliceObserver(observeInput, observeStart, observeEnd) { + var observeLength = makeSubObserver(observeEnd, observeStart); + var observeArrayView = makeReplacingViewObserver(observeValue, makeParentObserver(observeStart), makeParentObserver(observeLength)); + var observeStringSlice = makeStringSliceObserver(observeValue, makeParentObserver(observeStart), makeParentObserver(observeEnd)); + return function (emit, scope) { + return observeInput(function (input) { + if (typeof input === "string") { + return observeStringSlice(emit, scope.nest(input)); + } else if (Array.isArray(input)) { + return observeArrayView(emit, scope.nest(input)); + } else { + return emit(); + } + }, scope) + }; +} + +var makeStringSliceObserver = makeMethodObserverMaker("slice"); +var makeSubObserver = makeOperatorObserverMaker(Operators.sub); + var observeZero = makeLiteralObserver(0); exports.makeEnumerateObserver = makeNonReplacing(makeReplacingEnumerateObserver); diff --git a/spec/evaluate.js b/spec/evaluate.js index b4c38f4..07cae8b 100644 --- a/spec/evaluate.js +++ b/spec/evaluate.js @@ -304,6 +304,26 @@ module.exports = [ output: [3, 4] }, + { + path: "source.slice(start, end)", + input: { + source: [1, 2, 3, 4, 5], + start: 1, + end: 3 + }, + output: [2, 3] + }, + + { + path: "source.slice(start, end)", + input: { + source: "abcdefg", + start: 1, + end: 3 + }, + output: "bc" + }, + { path: "a && b", input: {a: true, b: true}, diff --git a/spec/readme-spec.js b/spec/readme-spec.js index 3c13360..87d4e2c 100644 --- a/spec/readme-spec.js +++ b/spec/readme-spec.js @@ -470,6 +470,40 @@ describe("Tutorial", function () { expect(controller.view).toEqual([5, 6, 7]); }); + it("Slice", function () { + var object = Bindings.defineBindings({ + start: 2, + end: 4 + }, { + slice: {"<-": "cake.slice(start, end)"} + }); + + object.cake = "abcdefg"; + expect(object.slice).toBe("cd"); + + object.cake = [1, 2, 3, 4, 5, 6, 7]; + expect(object.slice).toEqual([3, 4]); + + // Continued from above... + var original = object.slice; + object.cake.shift(); + expect(object.slice).toEqual([4, 5]); + expect(object.slice).toBe(original); + + // Continued from above... + object.start--; + object.end++; + expect(object.cake).toEqual([2, 3, 4, 5, 6, 7]); + expect(object.slice).toEqual([3, 4, 5, 6]); + expect(object.slice).toBe(original); + + // Continued from above... + object.cake = [7, 6, 5, 4, 3, 2, 1, 0]; + expect(object.slice).toEqual([6, 5, 4, 3]); + expect(object.slice).not.toBe(original); + + }); + it("Enumerate", function () { var object = {letters: ['a', 'b', 'c', 'd']}; bind(object, "lettersAtEvenIndexes", {