Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2108,6 +2188,7 @@ Bindings.defineBindings({
}, {
b: {
"<-": "a + 1",
trace: true
}
});
```
Expand Down
4 changes: 2 additions & 2 deletions bind.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
}
Expand Down
1 change: 1 addition & 0 deletions compile-observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions observers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 20 additions & 0 deletions spec/evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
34 changes: 34 additions & 0 deletions spec/readme-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down