synstate-dom 0.4.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.
Files changed (62) hide show
  1. package/README.md +4 -2
  2. package/dist/bind/bind-props.d.mts +4 -4
  3. package/dist/bind/bind-props.d.mts.map +1 -1
  4. package/dist/bind/bind-props.mjs +109 -39
  5. package/dist/bind/bind-props.mjs.map +1 -1
  6. package/dist/constant/constant.d.mts +2 -2
  7. package/dist/constant/derive.mjs.map +1 -1
  8. package/dist/constant/view.d.mts +3 -3
  9. package/dist/control/for.d.mts +2 -2
  10. package/dist/control/for.mjs.map +1 -1
  11. package/dist/control/if.d.mts +2 -2
  12. package/dist/control/longest-increasing-subsequence.d.mts +3 -3
  13. package/dist/control/match-optional.d.mts +4 -4
  14. package/dist/control/match-result.d.mts +5 -6
  15. package/dist/control/match-result.d.mts.map +1 -1
  16. package/dist/control/match-result.mjs +0 -1
  17. package/dist/control/match-result.mjs.map +1 -1
  18. package/dist/control/mount-switch.d.mts +5 -5
  19. package/dist/control/portal.d.mts +2 -2
  20. package/dist/index.d.mts +1 -0
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +1 -0
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/internal/bind-value.d.mts.map +1 -1
  25. package/dist/internal/bind-value.mjs +7 -8
  26. package/dist/internal/bind-value.mjs.map +1 -1
  27. package/dist/jsx/child.d.mts +2 -2
  28. package/dist/jsx/element-props.d.mts +2 -2
  29. package/dist/jsx/jsx.mjs +1 -1
  30. package/dist/jsx/jsx.mjs.map +1 -1
  31. package/dist/jsx/mountable.d.mts +4 -4
  32. package/dist/jsx-dev-runtime.d.mts +2 -2
  33. package/dist/scope/index.d.mts +2 -0
  34. package/dist/scope/index.d.mts.map +1 -0
  35. package/dist/scope/index.mjs +2 -0
  36. package/dist/scope/index.mjs.map +1 -0
  37. package/dist/scope/scope-effects.d.mts +25 -0
  38. package/dist/scope/scope-effects.d.mts.map +1 -0
  39. package/dist/scope/scope-effects.mjs +43 -0
  40. package/dist/scope/scope-effects.mjs.map +1 -0
  41. package/package.json +9 -9
  42. package/src/bind/bind-props.mts +169 -66
  43. package/src/constant/constant.mts +2 -2
  44. package/src/constant/derive.mts +4 -4
  45. package/src/constant/view.mts +3 -3
  46. package/src/control/for.mts +10 -10
  47. package/src/control/if.mts +2 -2
  48. package/src/control/longest-increasing-subsequence.mts +3 -3
  49. package/src/control/match-optional.mts +4 -4
  50. package/src/control/match-result.mts +5 -6
  51. package/src/control/mount-switch.mts +5 -5
  52. package/src/control/portal.mts +2 -2
  53. package/src/index.mts +1 -0
  54. package/src/internal/bind-value.mts +7 -13
  55. package/src/jsx/child.mts +2 -2
  56. package/src/jsx/element-props.mts +2 -2
  57. package/src/jsx/jsx.mts +5 -5
  58. package/src/jsx/mountable.mts +4 -4
  59. package/src/jsx-dev-runtime.mts +2 -2
  60. package/src/scope/index.mts +1 -0
  61. package/src/scope/scope-effects.mts +60 -0
  62. package/src/scope/scope-effects.test.mts +89 -0
@@ -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
- subscribeInScope(source, (value) => {
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
- subscribeInScope(bindable, write);
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
- const subscribeInScope = <A,>(
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
- const subscription = source.subscribe(write);
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
- * array are checked as they are mounted.
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
- * and one handler per event of `HTMLElementEventMap`.
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
- // is put back for a component: `For` takes its `key` function as a prop.
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';
@@ -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
- * when mounted, not the nodes themselves (SPEC.md 6.1).
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
- * creates new nodes.
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;
@@ -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
- * not used.
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
+ });