@manyducks.co/dolla 0.78.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +241 -25
- package/lib/classes/DebugHub.d.ts +1 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +606 -425
- package/lib/index.js.map +4 -4
- package/lib/markup.d.ts +17 -9
- package/lib/nodes/cond.d.ts +4 -3
- package/lib/nodes/html.d.ts +1 -1
- package/lib/nodes/observer.d.ts +3 -3
- package/lib/nodes/outlet.d.ts +3 -3
- package/lib/nodes/repeat.d.ts +9 -7
- package/lib/nodes/text.d.ts +3 -3
- package/lib/signals.d.ts +118 -0
- package/lib/signals.test.d.ts +1 -0
- package/lib/state.d.ts +3 -2
- package/lib/store.d.ts +2 -19
- package/lib/stores/dialog.d.ts +16 -14
- package/lib/stores/document.d.ts +5 -4
- package/lib/stores/language.d.ts +4 -4
- package/lib/stores/router.d.ts +5 -4
- package/lib/testing/makeMockFetch._test.d.ts +1 -0
- package/lib/testing/makeMockFetch.test_skip.d.ts +1 -0
- package/lib/testing/wrapStore._test.d.ts +1 -0
- package/lib/testing/wrapStore.test_skip.d.ts +1 -0
- package/lib/types.d.ts +16 -32
- package/lib/view.d.ts +18 -19
- package/notes/scratch.md +76 -0
- package/package.json +1 -1
- package/tests/signals.test.js +135 -0
- package/tests/state.test.js +0 -290
package/lib/view.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { type AppContext, type ElementContext } from "./app.js";
|
|
2
2
|
import { type DebugChannel } from "./classes/DebugHub.js";
|
|
3
3
|
import { type DOMHandle, type Markup } from "./markup.js";
|
|
4
|
-
import { type
|
|
4
|
+
import { type MaybeSignal, Signal, type SignalValues, type StopFunction } from "./signals.js";
|
|
5
5
|
import { type Store } from "./store.js";
|
|
6
6
|
import type { BuiltInStores } from "./types.js";
|
|
7
7
|
/**
|
|
8
8
|
* Any valid value that a View can return.
|
|
9
9
|
*/
|
|
10
|
-
export type ViewResult = Node |
|
|
10
|
+
export type ViewResult = Node | Signal<any> | Markup | Markup[] | null;
|
|
11
11
|
export type View<P> = (props: P, context: ViewContext) => ViewResult;
|
|
12
12
|
export interface ViewContext extends DebugChannel {
|
|
13
13
|
/**
|
|
14
14
|
* A string ID unique to this view.
|
|
15
15
|
*/
|
|
16
|
-
readonly
|
|
16
|
+
readonly uid: string;
|
|
17
17
|
/**
|
|
18
18
|
* Returns the shared instance of `store`.
|
|
19
19
|
*/
|
|
@@ -24,20 +24,32 @@ export interface ViewContext extends DebugChannel {
|
|
|
24
24
|
getStore<N extends keyof BuiltInStores>(name: N): BuiltInStores[N];
|
|
25
25
|
/**
|
|
26
26
|
* Runs `callback` just before this view is connected. DOM nodes are not yet attached to the page.
|
|
27
|
+
* @deprecated
|
|
27
28
|
*/
|
|
28
29
|
beforeConnect(callback: () => void): void;
|
|
29
30
|
/**
|
|
30
31
|
* Runs `callback` after this view is connected. DOM nodes are now attached to the page.
|
|
32
|
+
* @deprecated use onMount
|
|
31
33
|
*/
|
|
32
34
|
onConnected(callback: () => void): void;
|
|
33
35
|
/**
|
|
34
36
|
* Runs `callback` just before this view is disconnected. DOM nodes are still attached to the page.
|
|
37
|
+
* @deprecated
|
|
35
38
|
*/
|
|
36
39
|
beforeDisconnect(callback: () => void): void;
|
|
37
40
|
/**
|
|
38
41
|
* Runs `callback` after this view is disconnected. DOM nodes are no longer attached to the page.
|
|
42
|
+
* @deprecated use onUnmount
|
|
39
43
|
*/
|
|
40
44
|
onDisconnected(callback: () => void): void;
|
|
45
|
+
/**
|
|
46
|
+
* Registers a callback to run just after this view is mounted.
|
|
47
|
+
*/
|
|
48
|
+
onMount(callback: () => void): void;
|
|
49
|
+
/**
|
|
50
|
+
* Registers a callback to run just after this view is unmounted.
|
|
51
|
+
*/
|
|
52
|
+
onUnmount(callback: () => void): void;
|
|
41
53
|
/**
|
|
42
54
|
* The name of this view for logging and debugging purposes.
|
|
43
55
|
*/
|
|
@@ -47,23 +59,10 @@ export interface ViewContext extends DebugChannel {
|
|
|
47
59
|
*/
|
|
48
60
|
crash(error: Error): void;
|
|
49
61
|
/**
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
observe<T>(state: MaybeReadable<T>, callback: (currentValue: T) => void): void;
|
|
53
|
-
/**
|
|
54
|
-
* Observes a set of readable values while this view is connected.
|
|
55
|
-
* Calls `callback` with each value in the same order as `readables` each time any of their values change.
|
|
62
|
+
* Watch a set of signals. The callback is called when any of the signals receive a new value.
|
|
63
|
+
* Watchers will be stopped when this view is unmounted. Returns a function to stop watching early.
|
|
56
64
|
*/
|
|
57
|
-
|
|
58
|
-
observe<I1, I2>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, callback: (value1: I1, value2: I2) => void): void;
|
|
59
|
-
observe<I1, I2, I3>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, state3: MaybeReadable<I3>, callback: (value1: I1, value2: I2, value3: I3) => void): void;
|
|
60
|
-
observe<I1, I2, I3, I4>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, state3: MaybeReadable<I3>, state4: MaybeReadable<I4>, callback: (value1: I1, value2: I2, value3: I3, value4: I4) => void): void;
|
|
61
|
-
observe<I1, I2, I3, I4, I5>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, state3: MaybeReadable<I3>, state4: MaybeReadable<I4>, state5: MaybeReadable<I5>, callback: (value1: I1, value2: I2, value3: I3, value4: I4, value5: I5) => void): void;
|
|
62
|
-
observe<I1, I2, I3, I4, I5, I6>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, state3: MaybeReadable<I3>, state4: MaybeReadable<I4>, state5: MaybeReadable<I5>, state6: MaybeReadable<I6>, callback: (value1: I1, value2: I2, value3: I3, value4: I4, value5: I5, value6: I6) => void): void;
|
|
63
|
-
observe<I1, I2, I3, I4, I5, I6, I7>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, state3: MaybeReadable<I3>, state4: MaybeReadable<I4>, state5: MaybeReadable<I5>, state6: MaybeReadable<I6>, state7: MaybeReadable<I7>, callback: (value1: I1, value2: I2, value3: I3, value4: I4, value5: I5, value6: I6, value7: I7) => void): void;
|
|
64
|
-
observe<I1, I2, I3, I4, I5, I6, I7, I8>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, state3: MaybeReadable<I3>, state4: MaybeReadable<I4>, state5: MaybeReadable<I5>, state6: MaybeReadable<I6>, state7: MaybeReadable<I7>, state8: MaybeReadable<I8>, callback: (value1: I1, value2: I2, value3: I3, value4: I4, value5: I5, value6: I6, value7: I7, value8: I8) => void): void;
|
|
65
|
-
observe<I1, I2, I3, I4, I5, I6, I7, I8, I9>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, state3: MaybeReadable<I3>, state4: MaybeReadable<I4>, state5: MaybeReadable<I5>, state6: MaybeReadable<I6>, state7: MaybeReadable<I7>, state8: MaybeReadable<I8>, state9: MaybeReadable<I9>, callback: (value1: I1, value2: I2, value3: I3, value4: I4, value5: I5, value6: I6, value7: I7, value8: I8, value9: I9) => void): void;
|
|
66
|
-
observe<I1, I2, I3, I4, I5, I6, I7, I8, I9, I10>(state1: MaybeReadable<I1>, state2: MaybeReadable<I2>, state3: MaybeReadable<I3>, state4: MaybeReadable<I4>, state5: MaybeReadable<I5>, state6: MaybeReadable<I6>, state7: MaybeReadable<I7>, state8: MaybeReadable<I8>, state9: MaybeReadable<I9>, state10: MaybeReadable<I10>, callback: (value1: I1, value2: I2, value3: I3, value4: I4, value5: I5, value6: I6, value7: I7, value8: I8, value9: I9, value10: I10) => void): void;
|
|
65
|
+
watch<T extends MaybeSignal<any>[]>(signals: [...T], callback: (...values: SignalValues<T>) => void): StopFunction;
|
|
67
66
|
/**
|
|
68
67
|
* Returns a Markup element that displays this view's children.
|
|
69
68
|
*/
|
package/notes/scratch.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Scratch Note
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
// import { signal, computed } from "@manyducks.co/dolla";
|
|
5
|
+
|
|
6
|
+
function signal(initialValue, options = {}) {}
|
|
7
|
+
|
|
8
|
+
function computed();
|
|
9
|
+
|
|
10
|
+
function WhateverView(props, c) {
|
|
11
|
+
// IDEA: Have state, computed and effect be methods on the view context.
|
|
12
|
+
// PROBLEM: Then what are the types when passing as props? State? ComputedState? or just a generic Dynamic<T> for readable/writable?
|
|
13
|
+
|
|
14
|
+
// Context variables (replacement for stores)
|
|
15
|
+
c.set("name", {
|
|
16
|
+
value: 5,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Can get in the same context scope or on a child.
|
|
20
|
+
// Throws an error if value is not set.
|
|
21
|
+
c.get("name");
|
|
22
|
+
|
|
23
|
+
// If we use this $readable and set function pattern then we only have a single type of signal which is read-only.
|
|
24
|
+
// Take props as Signal<T> and deal with setting in callbacks. Simple and predictable.
|
|
25
|
+
const [$count, setCount] = signal(5);
|
|
26
|
+
|
|
27
|
+
const $doubled = derived($count, (value) => value * 2);
|
|
28
|
+
|
|
29
|
+
// const watcher = new SignalWatcher($count, (value) => {
|
|
30
|
+
// c.debug.log("watcher received value: " + value);
|
|
31
|
+
// });
|
|
32
|
+
|
|
33
|
+
// watcher.start();
|
|
34
|
+
// watcher.stop();
|
|
35
|
+
|
|
36
|
+
$count.get(); // returns the current value
|
|
37
|
+
setCount(10); // updates the value
|
|
38
|
+
|
|
39
|
+
// Observe and trigger side effects.
|
|
40
|
+
c.watch($count, (count) => {
|
|
41
|
+
c.debug.log(`The value of count is: ${count}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Exposes language and translation tools.
|
|
45
|
+
c.i18n.translate$("");
|
|
46
|
+
c.i18n.$language;
|
|
47
|
+
c.i18n.setLanguage();
|
|
48
|
+
|
|
49
|
+
// Exposes internal HTTP client.
|
|
50
|
+
c.http.get(); // put, post, patch, delete, etc.
|
|
51
|
+
|
|
52
|
+
// Exposes router helpers and variables.
|
|
53
|
+
c.route.go("/");
|
|
54
|
+
c.route.$params;
|
|
55
|
+
c.route.$path;
|
|
56
|
+
c.route.$query;
|
|
57
|
+
c.route.setQuery({
|
|
58
|
+
value: 1,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return html`
|
|
62
|
+
<h1>This is the template</h1>
|
|
63
|
+
|
|
64
|
+
${render($count, (count) => {
|
|
65
|
+
// Render rebuilds the markup within when any of the dependencies change.
|
|
66
|
+
return html`<p>The count is ${count}</p>`;
|
|
67
|
+
|
|
68
|
+
// Other view helpers are also provided as exports
|
|
69
|
+
repeat();
|
|
70
|
+
cond();
|
|
71
|
+
outlet();
|
|
72
|
+
portal();
|
|
73
|
+
})}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
|
|
4
|
+
import { signal, derive } from "../lib/index.js";
|
|
5
|
+
|
|
6
|
+
test("signal", (t) => {
|
|
7
|
+
const [$count, setCount] = signal(5);
|
|
8
|
+
|
|
9
|
+
const defaultWatcher = t.mock.fn();
|
|
10
|
+
const stopDefault = $count.watch(defaultWatcher);
|
|
11
|
+
|
|
12
|
+
const lazyWatcher = t.mock.fn();
|
|
13
|
+
const stopLazy = $count.watch(lazyWatcher, { lazy: true });
|
|
14
|
+
|
|
15
|
+
assert.equal(defaultWatcher.mock.callCount(), 1, "watcher is called immediately by default");
|
|
16
|
+
assert.equal(lazyWatcher.mock.callCount(), 0, "lazy watcher is not called immediately");
|
|
17
|
+
|
|
18
|
+
assert.equal($count.get(), 5, "get returns the initial value");
|
|
19
|
+
|
|
20
|
+
setCount(12);
|
|
21
|
+
|
|
22
|
+
assert.equal($count.get(), 12, "setter updates the signal value");
|
|
23
|
+
|
|
24
|
+
assert.equal(defaultWatcher.mock.callCount(), 2, "default watcher is called");
|
|
25
|
+
assert.equal(lazyWatcher.mock.callCount(), 1, "lazy watcher is called");
|
|
26
|
+
|
|
27
|
+
assert.deepStrictEqual(defaultWatcher.mock.calls[1].arguments, [12], "default watcher is called with new value");
|
|
28
|
+
assert.deepStrictEqual(lazyWatcher.mock.calls[0].arguments, [12], "lazy watcher is called with new value");
|
|
29
|
+
|
|
30
|
+
setCount(12);
|
|
31
|
+
|
|
32
|
+
assert.equal(defaultWatcher.mock.callCount(), 2, "default watcher was not called with same value");
|
|
33
|
+
assert.equal(lazyWatcher.mock.callCount(), 1, "lazy watcher was not called with same value");
|
|
34
|
+
|
|
35
|
+
stopDefault();
|
|
36
|
+
stopLazy();
|
|
37
|
+
|
|
38
|
+
setCount(51);
|
|
39
|
+
|
|
40
|
+
assert.equal(defaultWatcher.mock.callCount(), 2, "default watcher has not been called again");
|
|
41
|
+
assert.equal(lazyWatcher.mock.callCount(), 1, "lazy watcher has not been called again");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("derive", (t) => {
|
|
45
|
+
const [$one, setOne] = signal(5);
|
|
46
|
+
const [$two, setTwo] = signal(20);
|
|
47
|
+
|
|
48
|
+
const deriveSum = t.mock.fn((one, two) => one + two);
|
|
49
|
+
const deriveProduct = t.mock.fn((one, two) => one * two);
|
|
50
|
+
|
|
51
|
+
const $sum = derive([$one, $two], deriveSum);
|
|
52
|
+
const $product = derive([$one, $two], deriveProduct);
|
|
53
|
+
|
|
54
|
+
assert.equal($sum.get(), 25, "sum is calculated correctly");
|
|
55
|
+
assert.equal($product.get(), 100, "product is calculated correctly");
|
|
56
|
+
|
|
57
|
+
assert.equal(deriveSum.mock.callCount(), 1, "derive function has only been called once");
|
|
58
|
+
|
|
59
|
+
$sum.get();
|
|
60
|
+
$sum.get();
|
|
61
|
+
$sum.get();
|
|
62
|
+
|
|
63
|
+
assert.equal(deriveSum.mock.callCount(), 1, "derive function still only called once as dependencies haven't changed");
|
|
64
|
+
|
|
65
|
+
const defaultWatcher = t.mock.fn();
|
|
66
|
+
const stopDefault = $sum.watch(defaultWatcher);
|
|
67
|
+
|
|
68
|
+
const lazyWatcher = t.mock.fn();
|
|
69
|
+
const stopLazy = $product.watch(lazyWatcher, { lazy: true });
|
|
70
|
+
|
|
71
|
+
assert.equal(defaultWatcher.mock.callCount(), 1, "default watcher has been called");
|
|
72
|
+
assert.equal(lazyWatcher.mock.callCount(), 0, "lazy watcher has not been called yet");
|
|
73
|
+
|
|
74
|
+
assert.deepStrictEqual(defaultWatcher.mock.calls[0].arguments, [25], "default watcher was called with initial value");
|
|
75
|
+
|
|
76
|
+
setOne(6);
|
|
77
|
+
|
|
78
|
+
assert.equal(defaultWatcher.mock.callCount(), 2, "default watcher has been called");
|
|
79
|
+
assert.equal(lazyWatcher.mock.callCount(), 1, "lazy watcher has been called");
|
|
80
|
+
|
|
81
|
+
assert.deepStrictEqual(defaultWatcher.mock.calls[1].arguments, [26], "default watcher was called with new value");
|
|
82
|
+
assert.deepStrictEqual(lazyWatcher.mock.calls[0].arguments, [120], "lazy watcher was called with new value");
|
|
83
|
+
|
|
84
|
+
setTwo(20);
|
|
85
|
+
|
|
86
|
+
assert.equal(defaultWatcher.mock.callCount(), 2, "default watcher was not called with same value");
|
|
87
|
+
assert.equal(lazyWatcher.mock.callCount(), 1, "lazy watcher was not called with same value");
|
|
88
|
+
|
|
89
|
+
stopDefault();
|
|
90
|
+
stopLazy();
|
|
91
|
+
|
|
92
|
+
setOne(4);
|
|
93
|
+
|
|
94
|
+
assert.equal(defaultWatcher.mock.callCount(), 2, "default watcher was not called after stop");
|
|
95
|
+
assert.equal(lazyWatcher.mock.callCount(), 1, "lazy watcher was not called after stop");
|
|
96
|
+
|
|
97
|
+
assert.equal($sum.get(), 24, "sum is derived correctly");
|
|
98
|
+
assert.equal($product.get(), 80, "product is derived correctly");
|
|
99
|
+
|
|
100
|
+
assert.equal(deriveSum.mock.callCount(), 3, "sum has only been derived three times");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("derive nested signals", (t) => {
|
|
104
|
+
const [$value, setValue] = signal(5);
|
|
105
|
+
|
|
106
|
+
const [$object, setObject] = signal({
|
|
107
|
+
href: derive([$value], (value) => `/projects/${value}/test`),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// o.href here is itself a derived value
|
|
111
|
+
const $href = derive([$object], (o) => o.href);
|
|
112
|
+
|
|
113
|
+
const watcher = t.mock.fn();
|
|
114
|
+
const stop = $href.watch(watcher);
|
|
115
|
+
|
|
116
|
+
assert.equal(watcher.mock.callCount(), 1);
|
|
117
|
+
assert.deepStrictEqual(watcher.mock.calls[0].arguments, ["/projects/5/test"]);
|
|
118
|
+
|
|
119
|
+
// Update value which href depends on.
|
|
120
|
+
setValue(12);
|
|
121
|
+
|
|
122
|
+
assert.equal(watcher.mock.callCount(), 2);
|
|
123
|
+
assert.deepStrictEqual(watcher.mock.calls[1].arguments, ["/projects/12/test"]);
|
|
124
|
+
|
|
125
|
+
// Now set the original object and replace the derived href.
|
|
126
|
+
// See that watcher still receives the latest value.
|
|
127
|
+
setObject({
|
|
128
|
+
href: derive([$value], (value) => `/projects/${value}/changed`),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
assert.equal(watcher.mock.callCount(), 3);
|
|
132
|
+
assert.deepStrictEqual(watcher.mock.calls[2].arguments, ["/projects/12/changed"]);
|
|
133
|
+
|
|
134
|
+
stop();
|
|
135
|
+
});
|
package/tests/state.test.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { $, $$, observe, unwrap, isReadable, isWritable } from "../lib/index.js";
|
|
4
|
-
|
|
5
|
-
test("isReadable, isWritable: returns correct results", (t) => {
|
|
6
|
-
const $$writable = $$(5);
|
|
7
|
-
const $readable = $($$writable);
|
|
8
|
-
const $computed = $($readable, (x) => x * 2);
|
|
9
|
-
|
|
10
|
-
assert.strictEqual(isWritable($$writable), true);
|
|
11
|
-
assert.strictEqual(isReadable($$writable), true);
|
|
12
|
-
|
|
13
|
-
assert.strictEqual(isWritable($readable), false);
|
|
14
|
-
assert.strictEqual(isReadable($readable), true);
|
|
15
|
-
|
|
16
|
-
assert.strictEqual(isWritable($computed), false);
|
|
17
|
-
assert.strictEqual(isReadable($computed), true);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("readable, writable, computed: basic functionality", (t) => {
|
|
21
|
-
const $$writable = $$("writable");
|
|
22
|
-
const $readable = $("test");
|
|
23
|
-
const $x = $($readable, (v) => v.toUpperCase());
|
|
24
|
-
const $y = $($$writable);
|
|
25
|
-
|
|
26
|
-
const compute = t.mock.fn((x, y) => {
|
|
27
|
-
return x + y;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const $computed = $([$x, $y], compute);
|
|
31
|
-
|
|
32
|
-
assert.strictEqual($computed.get(), "TESTwritable");
|
|
33
|
-
|
|
34
|
-
const observer = t.mock.fn();
|
|
35
|
-
const stop = observe($$writable, observer); // Observer is called immediately with the current value.
|
|
36
|
-
|
|
37
|
-
$$writable.set("new value"); // First observed change (second call)
|
|
38
|
-
$$writable.set("new value 2"); // Second observed change (third call)
|
|
39
|
-
|
|
40
|
-
assert.strictEqual($$writable.get(), "new value 2");
|
|
41
|
-
assert.strictEqual($computed.get(), "TESTnew value 2");
|
|
42
|
-
|
|
43
|
-
assert.strictEqual(unwrap($$writable), "new value 2");
|
|
44
|
-
assert.strictEqual(unwrap("value"), "value");
|
|
45
|
-
|
|
46
|
-
stop();
|
|
47
|
-
|
|
48
|
-
$$writable.set("can you hear me now?"); // Sets value but is not observed.
|
|
49
|
-
|
|
50
|
-
assert.strictEqual($$writable.get(), "can you hear me now?");
|
|
51
|
-
assert.strictEqual($computed.get(), "TESTcan you hear me now?");
|
|
52
|
-
|
|
53
|
-
assert.strictEqual(observer.mock.calls.length, 3);
|
|
54
|
-
assert.deepEqual(observer.mock.calls[0].arguments, ["writable"]);
|
|
55
|
-
assert.deepEqual(observer.mock.calls[1].arguments, ["new value"]);
|
|
56
|
-
assert.deepEqual(observer.mock.calls[2].arguments, ["new value 2"]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("writable -> readable -> readable: chained transforms", (t) => {
|
|
60
|
-
const $$number = $$(5);
|
|
61
|
-
const $doubled = $($$number, (x) => x * 2);
|
|
62
|
-
const $quadrupled = $($doubled, (x) => x * 2);
|
|
63
|
-
const observer = t.mock.fn();
|
|
64
|
-
|
|
65
|
-
assert.strictEqual($doubled.get(), 10);
|
|
66
|
-
assert.strictEqual($quadrupled.get(), 20);
|
|
67
|
-
|
|
68
|
-
const stop = observe($quadrupled, observer);
|
|
69
|
-
|
|
70
|
-
assert.strictEqual(observer.mock.calls.length, 1);
|
|
71
|
-
assert.deepEqual(observer.mock.calls[0].arguments, [20]);
|
|
72
|
-
|
|
73
|
-
$$number.set(50);
|
|
74
|
-
|
|
75
|
-
assert.strictEqual(observer.mock.calls.length, 2);
|
|
76
|
-
assert.deepEqual(observer.mock.calls[1].arguments, [200]);
|
|
77
|
-
|
|
78
|
-
stop();
|
|
79
|
-
|
|
80
|
-
$$number.set(100);
|
|
81
|
-
|
|
82
|
-
assert.strictEqual($quadrupled.get(), 400);
|
|
83
|
-
|
|
84
|
-
// Not called again after stop().
|
|
85
|
-
assert.strictEqual(observer.mock.calls.length, 2);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("writable: update", (t) => {
|
|
89
|
-
const $$numbers = $$(["one", "two", "three"]);
|
|
90
|
-
|
|
91
|
-
const original = $$numbers.get();
|
|
92
|
-
|
|
93
|
-
$$numbers.update((list) => [...list, "four"]);
|
|
94
|
-
|
|
95
|
-
const updated = $$numbers.get();
|
|
96
|
-
|
|
97
|
-
assert.notDeepEqual(original, updated);
|
|
98
|
-
assert.strictEqual(updated.length, 4);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("readable, writable, computed: observer called with initial value when registered", (t) => {
|
|
102
|
-
const $$value = $$(1);
|
|
103
|
-
const $value = $(5);
|
|
104
|
-
const $doubled = $($value, (x) => x * 2);
|
|
105
|
-
const $multi = $([$value, $doubled], (x, y) => x + y);
|
|
106
|
-
|
|
107
|
-
// Writable
|
|
108
|
-
const wObserver = t.mock.fn();
|
|
109
|
-
const wStop = observe($$value, wObserver);
|
|
110
|
-
assert.strictEqual(wObserver.mock.calls.length, 1);
|
|
111
|
-
assert.deepEqual(wObserver.mock.calls[0].arguments, [1]);
|
|
112
|
-
wStop();
|
|
113
|
-
|
|
114
|
-
// Readable
|
|
115
|
-
const rObserver = t.mock.fn();
|
|
116
|
-
const rStop = observe($value, rObserver);
|
|
117
|
-
assert.strictEqual(rObserver.mock.calls.length, 1);
|
|
118
|
-
assert.deepEqual(rObserver.mock.calls[0].arguments, [5]);
|
|
119
|
-
rStop();
|
|
120
|
-
|
|
121
|
-
// Computed (single source)
|
|
122
|
-
const cObserver1 = t.mock.fn();
|
|
123
|
-
const cStop1 = observe($doubled, cObserver1);
|
|
124
|
-
assert.strictEqual(cObserver1.mock.calls.length, 1);
|
|
125
|
-
assert.deepEqual(cObserver1.mock.calls[0].arguments, [10]);
|
|
126
|
-
cStop1();
|
|
127
|
-
|
|
128
|
-
// Computed (multi source)
|
|
129
|
-
const cObserver2 = t.mock.fn();
|
|
130
|
-
const cStop2 = observe($multi, cObserver2);
|
|
131
|
-
assert.strictEqual(cObserver2.mock.calls.length, 1);
|
|
132
|
-
assert.deepEqual(cObserver2.mock.calls[0].arguments, [15]);
|
|
133
|
-
cStop2();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("computed: basic functionality", (t) => {
|
|
137
|
-
const $$one = $$(2);
|
|
138
|
-
const $$two = $$(4);
|
|
139
|
-
const $$three = $$(8);
|
|
140
|
-
|
|
141
|
-
const joinFirst = t.mock.fn((one, two) => {
|
|
142
|
-
return one + two;
|
|
143
|
-
});
|
|
144
|
-
const $first = $([$$one, $$two], joinFirst);
|
|
145
|
-
|
|
146
|
-
const joinSecond = t.mock.fn((one, two, three) => {
|
|
147
|
-
return one + two + three;
|
|
148
|
-
});
|
|
149
|
-
const $second = $([$$one, $$two, $$three], joinSecond);
|
|
150
|
-
|
|
151
|
-
assert.strictEqual($first.get(), 6);
|
|
152
|
-
assert.strictEqual($second.get(), 14);
|
|
153
|
-
|
|
154
|
-
assert.strictEqual(joinFirst.mock.callCount(), 1);
|
|
155
|
-
assert.strictEqual(joinSecond.mock.callCount(), 1);
|
|
156
|
-
|
|
157
|
-
const observer = t.mock.fn();
|
|
158
|
-
const stop = observe($second, observer);
|
|
159
|
-
|
|
160
|
-
assert.strictEqual(observer.mock.callCount(), 1);
|
|
161
|
-
assert.deepEqual(observer.mock.calls[0].arguments, [14]); // Observer receives initial value.
|
|
162
|
-
assert.strictEqual(joinSecond.mock.callCount(), 2);
|
|
163
|
-
|
|
164
|
-
$$two.set(16);
|
|
165
|
-
|
|
166
|
-
assert.strictEqual(joinFirst.mock.callCount(), 1);
|
|
167
|
-
assert.strictEqual(joinSecond.mock.callCount(), 3);
|
|
168
|
-
|
|
169
|
-
assert.strictEqual($first.get(), 18);
|
|
170
|
-
assert.strictEqual($second.get(), 26);
|
|
171
|
-
|
|
172
|
-
assert.strictEqual(joinFirst.mock.callCount(), 2);
|
|
173
|
-
assert.strictEqual(joinSecond.mock.callCount(), 3);
|
|
174
|
-
|
|
175
|
-
assert.strictEqual(observer.mock.callCount(), 2); // Observer received value.
|
|
176
|
-
assert.deepEqual(observer.mock.calls[1].arguments, [26]);
|
|
177
|
-
|
|
178
|
-
stop();
|
|
179
|
-
|
|
180
|
-
$$one.set(32);
|
|
181
|
-
|
|
182
|
-
assert.strictEqual($first.get(), 48);
|
|
183
|
-
assert.strictEqual($second.get(), 56);
|
|
184
|
-
|
|
185
|
-
assert.strictEqual(joinFirst.mock.callCount(), 3);
|
|
186
|
-
assert.strictEqual(joinSecond.mock.callCount(), 4);
|
|
187
|
-
|
|
188
|
-
assert.strictEqual(observer.mock.callCount(), 2); // Not called after stop()
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("computed: observers received value when undefined", (t) => {
|
|
192
|
-
const $$one = $$(true);
|
|
193
|
-
const $$two = $$(false);
|
|
194
|
-
|
|
195
|
-
const $joined = $([$$one, $$two], (one, two) => {
|
|
196
|
-
if (one && two) {
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
if (!one && !two) {
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
assert.strictEqual($joined.get(), undefined);
|
|
205
|
-
|
|
206
|
-
const observer = t.mock.fn();
|
|
207
|
-
const stop = observe($joined, observer);
|
|
208
|
-
|
|
209
|
-
assert.strictEqual(observer.mock.calls.length, 1);
|
|
210
|
-
assert.deepEqual(observer.mock.calls[0].arguments, [undefined]);
|
|
211
|
-
|
|
212
|
-
$$two.set(true);
|
|
213
|
-
|
|
214
|
-
assert.strictEqual($joined.get(), true);
|
|
215
|
-
assert.strictEqual(observer.mock.calls.length, 2);
|
|
216
|
-
assert.deepEqual(observer.mock.calls[1].arguments, [true]);
|
|
217
|
-
|
|
218
|
-
$$one.set(false);
|
|
219
|
-
|
|
220
|
-
assert.strictEqual($joined.get(), undefined);
|
|
221
|
-
assert.strictEqual(observer.mock.calls.length, 3);
|
|
222
|
-
assert.deepEqual(observer.mock.calls[2].arguments, [undefined]);
|
|
223
|
-
|
|
224
|
-
stop();
|
|
225
|
-
|
|
226
|
-
$$two.set(false);
|
|
227
|
-
|
|
228
|
-
assert.strictEqual($joined.get(), false);
|
|
229
|
-
|
|
230
|
-
assert.strictEqual(observer.mock.calls.length, 3); // Not called again after stop()
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("computed: observer only gets new values when they are different", (t) => {
|
|
234
|
-
const $$source = $$({ name: "Jimbo Jones", age: 346 });
|
|
235
|
-
const $age = $($$source, (x) => x.age);
|
|
236
|
-
|
|
237
|
-
const ageObserver = t.mock.fn();
|
|
238
|
-
const stop = observe($age, ageObserver);
|
|
239
|
-
|
|
240
|
-
assert.strictEqual(ageObserver.mock.calls.length, 1);
|
|
241
|
-
assert.deepEqual(ageObserver.mock.calls[0].arguments, [346]);
|
|
242
|
-
|
|
243
|
-
$$source.update((current) => {
|
|
244
|
-
return { ...current, name: "Not Jimbo Jones" };
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
assert.strictEqual(ageObserver.mock.calls.length, 1); // Age has not changed, so shouldn't be observed again.
|
|
248
|
-
|
|
249
|
-
$$source.update((current) => {
|
|
250
|
-
return { ...current, age: 347 };
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
assert.strictEqual(ageObserver.mock.calls.length, 2); // Age change should have been observed.
|
|
254
|
-
assert.deepEqual(ageObserver.mock.calls[1].arguments, [347]);
|
|
255
|
-
|
|
256
|
-
stop();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("proxy", (t) => {
|
|
260
|
-
const $$numbers = $$([1, 2, 3]);
|
|
261
|
-
const $$hasTwo = $$($$numbers, {
|
|
262
|
-
get() {
|
|
263
|
-
return $$numbers.get().includes(2);
|
|
264
|
-
},
|
|
265
|
-
set(value) {
|
|
266
|
-
$$numbers.update((numbers) => {
|
|
267
|
-
if (value && !numbers.includes(2)) {
|
|
268
|
-
return [...numbers, 2].sort();
|
|
269
|
-
}
|
|
270
|
-
if (!value && numbers.includes(2)) {
|
|
271
|
-
return numbers.filter((n) => n !== 2);
|
|
272
|
-
}
|
|
273
|
-
return numbers;
|
|
274
|
-
});
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
assert.deepEqual($$numbers.get(), [1, 2, 3]);
|
|
279
|
-
assert.strictEqual($$hasTwo.get(), true, "numbers should contain 2");
|
|
280
|
-
|
|
281
|
-
$$hasTwo.set(false);
|
|
282
|
-
|
|
283
|
-
assert.deepEqual($$numbers.get(), [1, 3]);
|
|
284
|
-
assert.strictEqual($$hasTwo.get(), false, "numbers should not contain 2");
|
|
285
|
-
|
|
286
|
-
$$hasTwo.set(true);
|
|
287
|
-
|
|
288
|
-
assert.deepEqual($$numbers.get(), [1, 2, 3]);
|
|
289
|
-
assert.strictEqual($$hasTwo.get(), true, "numbers should again contain 2");
|
|
290
|
-
});
|