synstate-dom 0.3.0 → 0.5.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 +4 -2
- package/dist/bind/bind-props.d.mts +4 -4
- package/dist/bind/bind-props.d.mts.map +1 -1
- package/dist/bind/bind-props.mjs +109 -39
- package/dist/bind/bind-props.mjs.map +1 -1
- package/dist/constant/constant.d.mts +2 -2
- package/dist/constant/derive.d.mts +2 -1
- package/dist/constant/derive.d.mts.map +1 -1
- package/dist/constant/derive.mjs +5 -5
- package/dist/constant/derive.mjs.map +1 -1
- package/dist/constant/view.d.mts +3 -3
- package/dist/control/for.d.mts +2 -2
- package/dist/control/for.mjs.map +1 -1
- package/dist/control/if.d.mts +2 -2
- package/dist/control/longest-increasing-subsequence.d.mts +3 -3
- package/dist/control/match-optional.d.mts +4 -4
- package/dist/control/match-result.d.mts +5 -6
- package/dist/control/match-result.d.mts.map +1 -1
- package/dist/control/match-result.mjs +0 -1
- package/dist/control/match-result.mjs.map +1 -1
- package/dist/control/mount-switch.d.mts +5 -5
- package/dist/control/portal.d.mts +2 -2
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/internal/bind-value.d.mts.map +1 -1
- package/dist/internal/bind-value.mjs +7 -8
- package/dist/internal/bind-value.mjs.map +1 -1
- package/dist/jsx/child.d.mts +2 -2
- package/dist/jsx/element-props.d.mts +2 -2
- package/dist/jsx/jsx.mjs +1 -1
- package/dist/jsx/jsx.mjs.map +1 -1
- package/dist/jsx/mountable.d.mts +4 -4
- package/dist/jsx-dev-runtime.d.mts +2 -2
- package/dist/scope/index.d.mts +2 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +2 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/scope/scope-effects.d.mts +25 -0
- package/dist/scope/scope-effects.d.mts.map +1 -0
- package/dist/scope/scope-effects.mjs +43 -0
- package/dist/scope/scope-effects.mjs.map +1 -0
- package/package.json +10 -10
- package/src/bind/bind-props.mts +169 -66
- package/src/constant/constant.mts +2 -2
- package/src/constant/derive.mts +11 -12
- package/src/constant/derive.test.mts +25 -1
- package/src/constant/view.mts +3 -3
- package/src/control/for.mts +10 -10
- package/src/control/if.mts +2 -2
- package/src/control/longest-increasing-subsequence.mts +3 -3
- package/src/control/match-optional.mts +4 -4
- package/src/control/match-result.mts +5 -6
- package/src/control/mount-switch.mts +5 -5
- package/src/control/portal.mts +2 -2
- package/src/index.mts +1 -0
- package/src/internal/bind-value.mts +7 -13
- package/src/jsx/child.mts +2 -2
- package/src/jsx/element-props.mts +2 -2
- package/src/jsx/jsx.mts +5 -5
- package/src/jsx/mountable.mts +4 -4
- package/src/jsx-dev-runtime.mts +2 -2
- package/src/scope/index.mts +1 -0
- package/src/scope/scope-effects.mts +60 -0
- package/src/scope/scope-effects.test.mts +89 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The positions in `values` of one longest strictly increasing subsequence,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The positions in `values` of one longest strictly increasing subsequence, in
|
|
3
|
+
* O(n log n). `For` keeps the rows at these positions in place and moves only
|
|
4
|
+
* the others (SPEC.md 8.4).
|
|
5
5
|
*/
|
|
6
6
|
export const longestIncreasingSubsequence = (
|
|
7
7
|
values: readonly number[],
|
|
@@ -13,10 +13,10 @@ import { mountSwitch } from './mount-switch.mjs';
|
|
|
13
13
|
* Mounts `some` while `value` holds a value and `none` while it is empty
|
|
14
14
|
* (SPEC.md 8.3).
|
|
15
15
|
*
|
|
16
|
-
* `some` is called once per change from empty to present, with an observable
|
|
17
|
-
*
|
|
18
|
-
* through that observable rather than rebuilding the branch. It is derived
|
|
19
|
-
*
|
|
16
|
+
* `some` is called once per change from empty to present, with an observable of
|
|
17
|
+
* the value narrowed to `T`. While the value stays present, new values flow
|
|
18
|
+
* through that observable rather than rebuilding the branch. It is derived from
|
|
19
|
+
* `value`, not written to, so nothing is written during propagation.
|
|
20
20
|
*/
|
|
21
21
|
export const MatchOptional = <T,>({
|
|
22
22
|
value,
|
|
@@ -10,13 +10,12 @@ import { mountable, type JSX } from '../jsx/index.mjs';
|
|
|
10
10
|
import { mountSwitch } from './mount-switch.mjs';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Mounts `ok` while `value` is `Ok` and `err` while it is `Err`
|
|
14
|
-
* (SPEC.md 8.3).
|
|
13
|
+
* Mounts `ok` while `value` is `Ok` and `err` while it is `Err` (SPEC.md 8.3).
|
|
15
14
|
*
|
|
16
|
-
* Each branch is called once per change of tag, with an observable of the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* Each branch is called once per change of tag, with an observable of the value
|
|
16
|
+
* narrowed to `S` or `E`; while the tag stays the same, new values flow through
|
|
17
|
+
* that observable rather than rebuilding the branch. It is derived from
|
|
18
|
+
* `value`, not written to, so nothing is written during propagation.
|
|
20
19
|
*/
|
|
21
20
|
export const MatchResult = <S, E>({
|
|
22
21
|
value,
|
|
@@ -14,11 +14,11 @@ import { toNodes, type Mountable, type MountedNodes } from '../jsx/index.mjs';
|
|
|
14
14
|
* the value of `value` (SPEC.md 8.1–8.3).
|
|
15
15
|
*
|
|
16
16
|
* The branch is rebuilt only when `keyOf` gives a different key: within the
|
|
17
|
-
* same key the branch keeps its nodes and its scope, and it is up to the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* same key the branch keeps its nodes and its scope, and it is up to the branch
|
|
18
|
+
* to follow the value (`MatchOptional` hands it an observable derived from
|
|
19
|
+
* `value`). Each branch is mounted in a scope of its own, a child of the scope
|
|
20
|
+
* the switch was mounted in; disposing the branch's scope removes its nodes, so
|
|
21
|
+
* the switch's own nodes stay accurate however often it switches.
|
|
22
22
|
*
|
|
23
23
|
* `branch` runs inside the new scope, so whatever it creates belongs to the
|
|
24
24
|
* branch.
|
package/src/control/portal.mts
CHANGED
|
@@ -3,8 +3,8 @@ import { mountChild, mountable, type Child, type JSX } from '../jsx/index.mjs';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Mounts `children` into `target` (`document.body` when omitted) rather than
|
|
6
|
-
* where it is written (SPEC.md 8.6): for a modal, a tooltip or a toast that
|
|
7
|
-
*
|
|
6
|
+
* where it is written (SPEC.md 8.6): for a modal, a tooltip or a toast that has
|
|
7
|
+
* to escape an ancestor's `overflow: hidden` or `z-index`.
|
|
8
8
|
*
|
|
9
9
|
* The children belong to the scope where `Portal` is written, so closing the
|
|
10
10
|
* enclosing `If` branch removes them too. Nothing is left in the place it is
|
package/src/index.mts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isObservable,
|
|
3
|
-
onScopeDispose,
|
|
4
|
-
type InitializedObservable,
|
|
5
|
-
} from 'synstate';
|
|
1
|
+
import { isObservable, type InitializedObservable } from 'synstate';
|
|
6
2
|
import type { Bindable } from '../constant/index.mjs';
|
|
3
|
+
import { subscribeInScope } from '../scope/index.mjs';
|
|
7
4
|
import { isView } from './is-view.mjs';
|
|
8
5
|
|
|
9
6
|
/**
|
|
@@ -23,7 +20,7 @@ export const bindValue = <T,>(
|
|
|
23
20
|
): void => {
|
|
24
21
|
if (isView(bindable)) {
|
|
25
22
|
bindable.open((source, fn) => {
|
|
26
|
-
|
|
23
|
+
bindObservable(source, (value) => {
|
|
27
24
|
write(fn(value));
|
|
28
25
|
});
|
|
29
26
|
});
|
|
@@ -32,7 +29,7 @@ export const bindValue = <T,>(
|
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
if (isObservable(bindable)) {
|
|
35
|
-
|
|
32
|
+
bindObservable(bindable, write);
|
|
36
33
|
|
|
37
34
|
return;
|
|
38
35
|
}
|
|
@@ -40,7 +37,8 @@ export const bindValue = <T,>(
|
|
|
40
37
|
write(bindable);
|
|
41
38
|
};
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
/** A completed observable is written once and not subscribed to. */
|
|
41
|
+
const bindObservable = <A,>(
|
|
44
42
|
source: InitializedObservable<A>,
|
|
45
43
|
write: (value: A) => void,
|
|
46
44
|
): void => {
|
|
@@ -50,9 +48,5 @@ const subscribeInScope = <A,>(
|
|
|
50
48
|
return;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
onScopeDispose(() => {
|
|
56
|
-
subscription.unsubscribe();
|
|
57
|
-
});
|
|
51
|
+
subscribeInScope(source, write);
|
|
58
52
|
};
|
package/src/jsx/child.mts
CHANGED
|
@@ -37,8 +37,8 @@ export const toNodes = (nodes: MountedNodes): readonly ChildNode[] =>
|
|
|
37
37
|
Arr.isArray(nodes) ? nodes : ([nodes] as const);
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Tells a child from any other value, one level deep: the elements of an
|
|
41
|
-
*
|
|
40
|
+
* Tells a child from any other value, one level deep: the elements of an array
|
|
41
|
+
* are checked as they are mounted.
|
|
42
42
|
*/
|
|
43
43
|
export const isChild = (value: unknown): value is Child =>
|
|
44
44
|
Arr.isArray(value) ||
|
|
@@ -43,8 +43,8 @@ export type StyleObject = ReadonlyRecord<string, Bindable<string | undefined>>;
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* The props of an HTML element: the common ones, one `Bindable` per writable
|
|
46
|
-
* primitive DOM property of `E` (`id`, `disabled`, `value`, `tabIndex`, …),
|
|
47
|
-
*
|
|
46
|
+
* primitive DOM property of `E` (`id`, `disabled`, `value`, `tabIndex`, …), and
|
|
47
|
+
* one handler per event of `HTMLElementEventMap`.
|
|
48
48
|
*
|
|
49
49
|
* An attribute, `aria-*` and `data-*` included, also takes `undefined`, which
|
|
50
50
|
* removes it: a value that comes and goes is an observable of `T | undefined`
|
package/src/jsx/jsx.mts
CHANGED
|
@@ -19,8 +19,8 @@ export const createMountable = <P,>(
|
|
|
19
19
|
props: P,
|
|
20
20
|
key?: unknown,
|
|
21
21
|
): Mountable => {
|
|
22
|
-
// The transform takes `key` out of the props and passes it on its own. It
|
|
23
|
-
//
|
|
22
|
+
// The transform takes `key` out of the props and passes it on its own. It is
|
|
23
|
+
// put back for a component: `For` takes its `key` function as a prop.
|
|
24
24
|
const propsWithKey = key === undefined ? props : ({ ...props, key } as const);
|
|
25
25
|
|
|
26
26
|
return typeof type === 'string'
|
|
@@ -32,6 +32,9 @@ export const createMountable = <P,>(
|
|
|
32
32
|
export const Fragment = (props: Readonly<{ children?: Child }>): Mountable =>
|
|
33
33
|
mountable(() => mountChild(props.children ?? []));
|
|
34
34
|
|
|
35
|
+
const isRefCallback = (value: unknown): value is (element: Element) => void =>
|
|
36
|
+
typeof value === 'function';
|
|
37
|
+
|
|
35
38
|
const mountElement = (tag: string, props: unknown): Element => {
|
|
36
39
|
const element = svgTagNameSet.has(tag)
|
|
37
40
|
? document.createElementNS(svgNamespace, tag)
|
|
@@ -58,6 +61,3 @@ const mountElement = (tag: string, props: unknown): Element => {
|
|
|
58
61
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
|
59
62
|
|
|
60
63
|
const svgTagNameSet: ReadonlySet<string> = new Set(svgTagNames);
|
|
61
|
-
|
|
62
|
-
const isRefCallback = (value: unknown): value is (element: Element) => void =>
|
|
63
|
-
typeof value === 'function';
|
package/src/jsx/mountable.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mountableTag } from '../internal/index.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* What a JSX expression evaluates to: a description that creates DOM nodes
|
|
5
|
-
*
|
|
4
|
+
* What a JSX expression evaluates to: a description that creates DOM nodes when
|
|
5
|
+
* mounted, not the nodes themselves (SPEC.md 6.1).
|
|
6
6
|
*
|
|
7
7
|
* The children of a JSX element are evaluated before the element, so a
|
|
8
8
|
* description that created its nodes at once would create both branches of
|
|
@@ -11,8 +11,8 @@ import { mountableTag } from '../internal/index.mjs';
|
|
|
11
11
|
* It is an object rather than a function: a props object with a `then` prop
|
|
12
12
|
* holding a function would be taken for a thenable.
|
|
13
13
|
*
|
|
14
|
-
* The same description may be mounted any number of times; each mount
|
|
15
|
-
*
|
|
14
|
+
* The same description may be mounted any number of times; each mount creates
|
|
15
|
+
* new nodes.
|
|
16
16
|
*/
|
|
17
17
|
export type Mountable = Readonly<{
|
|
18
18
|
[mountableTag]: true;
|
package/src/jsx-dev-runtime.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The module `"jsxImportSource": "synstate-dom"` makes TypeScript import for
|
|
3
|
-
* `"jsx": "react-jsxdev"`. The extra arguments (`key`, source location) are
|
|
4
|
-
*
|
|
3
|
+
* `"jsx": "react-jsxdev"`. The extra arguments (`key`, source location) are not
|
|
4
|
+
* used.
|
|
5
5
|
*/
|
|
6
6
|
export { Fragment, createMountable as jsxDEV, type JSX } from './jsx/index.mjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './scope-effects.mjs';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { onScopeDispose, type Observable } from 'synstate';
|
|
2
|
+
import type { StrictPick } from 'ts-type-forge';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calls `next` with every value of `source`, the current one first, until the
|
|
6
|
+
* current scope is disposed — what a component that is called once does in
|
|
7
|
+
* place of an effect.
|
|
8
|
+
*/
|
|
9
|
+
export const subscribeInScope = <A,>(
|
|
10
|
+
source: Observable<A>,
|
|
11
|
+
next: (value: A) => void,
|
|
12
|
+
): void => {
|
|
13
|
+
const subscription = source.subscribe(next);
|
|
14
|
+
|
|
15
|
+
onScopeDispose(() => {
|
|
16
|
+
subscription.unsubscribe();
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Listens to `type` on `target` until the current scope is disposed. */
|
|
21
|
+
export const listenInScope = (
|
|
22
|
+
target: Readonly<
|
|
23
|
+
StrictPick<EventTarget, 'addEventListener' | 'removeEventListener'>
|
|
24
|
+
>,
|
|
25
|
+
type: string,
|
|
26
|
+
listener: (event: Event) => void,
|
|
27
|
+
options?: Readonly<{ capture?: boolean; passive?: boolean }>,
|
|
28
|
+
): void => {
|
|
29
|
+
target.addEventListener(type, listener, options);
|
|
30
|
+
|
|
31
|
+
onScopeDispose(() => {
|
|
32
|
+
target.removeEventListener(type, listener, options);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Makes the function that runs work once the update under way has finished:
|
|
38
|
+
* in a microtask, so before the next paint, with the DOM bound to the update
|
|
39
|
+
* already written — and nothing written to a state while another is still
|
|
40
|
+
* propagating, which synstate warns of. Work for a scope disposed by then is
|
|
41
|
+
* dropped.
|
|
42
|
+
*
|
|
43
|
+
* Also what runs after the first mount: a component's nodes are attached to the
|
|
44
|
+
* document only once the whole description they are in is mounted.
|
|
45
|
+
*/
|
|
46
|
+
export const createAfterUpdate = (): ((work: () => void) => void) => {
|
|
47
|
+
let mut_alive = true;
|
|
48
|
+
|
|
49
|
+
onScopeDispose(() => {
|
|
50
|
+
mut_alive = false;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (work) => {
|
|
54
|
+
queueMicrotask(() => {
|
|
55
|
+
if (mut_alive) {
|
|
56
|
+
work();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createScope, createState } from 'synstate';
|
|
2
|
+
import {
|
|
3
|
+
createAfterUpdate,
|
|
4
|
+
listenInScope,
|
|
5
|
+
subscribeInScope,
|
|
6
|
+
} from './scope-effects.mjs';
|
|
7
|
+
|
|
8
|
+
describe(subscribeInScope, () => {
|
|
9
|
+
test('follows the source, the current value first, until the scope is disposed', () => {
|
|
10
|
+
const scope = createScope(undefined);
|
|
11
|
+
|
|
12
|
+
const [source, setSource] = createState(0);
|
|
13
|
+
|
|
14
|
+
const mut_received: number[] = [];
|
|
15
|
+
|
|
16
|
+
scope.run(() => {
|
|
17
|
+
subscribeInScope(source, (value) => {
|
|
18
|
+
mut_received.push(value);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
setSource(1);
|
|
23
|
+
|
|
24
|
+
scope.dispose();
|
|
25
|
+
|
|
26
|
+
setSource(2);
|
|
27
|
+
|
|
28
|
+
assert.deepStrictEqual(mut_received, [0, 1]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe(createAfterUpdate, () => {
|
|
33
|
+
test('runs the work in a microtask, and drops it once the scope is disposed', async () => {
|
|
34
|
+
const scope = createScope(undefined);
|
|
35
|
+
|
|
36
|
+
const afterUpdate = scope.run(createAfterUpdate);
|
|
37
|
+
|
|
38
|
+
const mut_log: string[] = [];
|
|
39
|
+
|
|
40
|
+
afterUpdate(() => {
|
|
41
|
+
mut_log.push('first');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.deepStrictEqual(mut_log, []);
|
|
45
|
+
|
|
46
|
+
await Promise.resolve();
|
|
47
|
+
|
|
48
|
+
assert.deepStrictEqual(mut_log, ['first']);
|
|
49
|
+
|
|
50
|
+
afterUpdate(() => {
|
|
51
|
+
mut_log.push('second');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
scope.dispose();
|
|
55
|
+
|
|
56
|
+
await Promise.resolve();
|
|
57
|
+
|
|
58
|
+
assert.deepStrictEqual(mut_log, ['first']);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe(listenInScope, () => {
|
|
63
|
+
test('listens, with the options it is given, until the scope is disposed', () => {
|
|
64
|
+
const scope = createScope(undefined);
|
|
65
|
+
|
|
66
|
+
const target = new EventTarget();
|
|
67
|
+
|
|
68
|
+
const mut_received: string[] = [];
|
|
69
|
+
|
|
70
|
+
scope.run(() => {
|
|
71
|
+
listenInScope(
|
|
72
|
+
target,
|
|
73
|
+
'ping',
|
|
74
|
+
(event) => {
|
|
75
|
+
mut_received.push(event.type);
|
|
76
|
+
},
|
|
77
|
+
{ capture: true },
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
target.dispatchEvent(new Event('ping'));
|
|
82
|
+
|
|
83
|
+
scope.dispose();
|
|
84
|
+
|
|
85
|
+
target.dispatchEvent(new Event('ping'));
|
|
86
|
+
|
|
87
|
+
assert.deepStrictEqual(mut_received, ['ping']);
|
|
88
|
+
});
|
|
89
|
+
});
|