synstate-dom 0.0.0-stage → 0.2.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 (185) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +218 -2
  3. package/dist/bind/bind-props.d.mts +9 -0
  4. package/dist/bind/bind-props.d.mts.map +1 -0
  5. package/dist/bind/bind-props.mjs +117 -0
  6. package/dist/bind/bind-props.mjs.map +1 -0
  7. package/dist/bind/index.d.mts +2 -0
  8. package/dist/bind/index.d.mts.map +1 -0
  9. package/dist/bind/index.mjs +2 -0
  10. package/dist/bind/index.mjs.map +1 -0
  11. package/dist/constant/bindable.d.mts +11 -0
  12. package/dist/constant/bindable.d.mts.map +1 -0
  13. package/dist/constant/bindable.mjs +2 -0
  14. package/dist/constant/bindable.mjs.map +1 -0
  15. package/dist/constant/constant.d.mts +20 -0
  16. package/dist/constant/constant.d.mts.map +1 -0
  17. package/dist/constant/constant.mjs +10 -0
  18. package/dist/constant/constant.mjs.map +1 -0
  19. package/dist/constant/derive.d.mts +16 -0
  20. package/dist/constant/derive.d.mts.map +1 -0
  21. package/dist/constant/derive.mjs +28 -0
  22. package/dist/constant/derive.mjs.map +1 -0
  23. package/dist/constant/index.d.mts +7 -0
  24. package/dist/constant/index.d.mts.map +1 -0
  25. package/dist/constant/index.mjs +7 -0
  26. package/dist/constant/index.mjs.map +1 -0
  27. package/dist/constant/map-bindable.d.mts +10 -0
  28. package/dist/constant/map-bindable.d.mts.map +1 -0
  29. package/dist/constant/map-bindable.mjs +16 -0
  30. package/dist/constant/map-bindable.mjs.map +1 -0
  31. package/dist/constant/to-observable.d.mts +12 -0
  32. package/dist/constant/to-observable.d.mts.map +1 -0
  33. package/dist/constant/to-observable.mjs +16 -0
  34. package/dist/constant/to-observable.mjs.map +1 -0
  35. package/dist/constant/view.d.mts +25 -0
  36. package/dist/constant/view.d.mts.map +1 -0
  37. package/dist/constant/view.mjs +15 -0
  38. package/dist/constant/view.mjs.map +1 -0
  39. package/dist/control/for.d.mts +29 -0
  40. package/dist/control/for.d.mts.map +1 -0
  41. package/dist/control/for.mjs +201 -0
  42. package/dist/control/for.mjs.map +1 -0
  43. package/dist/control/if.d.mts +16 -0
  44. package/dist/control/if.d.mts.map +1 -0
  45. package/dist/control/if.mjs +12 -0
  46. package/dist/control/if.mjs.map +1 -0
  47. package/dist/control/index.d.mts +6 -0
  48. package/dist/control/index.d.mts.map +1 -0
  49. package/dist/control/index.mjs +6 -0
  50. package/dist/control/index.mjs.map +1 -0
  51. package/dist/control/longest-increasing-subsequence.d.mts +7 -0
  52. package/dist/control/longest-increasing-subsequence.d.mts.map +1 -0
  53. package/dist/control/longest-increasing-subsequence.mjs +37 -0
  54. package/dist/control/longest-increasing-subsequence.mjs.map +1 -0
  55. package/dist/control/match-optional.d.mts +19 -0
  56. package/dist/control/match-optional.d.mts.map +1 -0
  57. package/dist/control/match-optional.mjs +21 -0
  58. package/dist/control/match-optional.mjs.map +1 -0
  59. package/dist/control/match-result.d.mts +19 -0
  60. package/dist/control/match-result.d.mts.map +1 -0
  61. package/dist/control/match-result.mjs +27 -0
  62. package/dist/control/match-result.mjs.map +1 -0
  63. package/dist/control/mount-switch.d.mts +18 -0
  64. package/dist/control/mount-switch.d.mts.map +1 -0
  65. package/dist/control/mount-switch.mjs +51 -0
  66. package/dist/control/mount-switch.mjs.map +1 -0
  67. package/dist/control/portal.d.mts +15 -0
  68. package/dist/control/portal.d.mts.map +1 -0
  69. package/dist/control/portal.mjs +22 -0
  70. package/dist/control/portal.mjs.map +1 -0
  71. package/dist/index.d.mts +5 -0
  72. package/dist/index.d.mts.map +1 -0
  73. package/dist/index.mjs +5 -0
  74. package/dist/index.mjs.map +1 -0
  75. package/dist/internal/as-constant.d.mts +9 -0
  76. package/dist/internal/as-constant.d.mts.map +1 -0
  77. package/dist/internal/as-constant.mjs +10 -0
  78. package/dist/internal/as-constant.mjs.map +1 -0
  79. package/dist/internal/bind-value.d.mts +14 -0
  80. package/dist/internal/bind-value.d.mts.map +1 -0
  81. package/dist/internal/bind-value.mjs +39 -0
  82. package/dist/internal/bind-value.mjs.map +1 -0
  83. package/dist/internal/dom-tables.d.mts +30 -0
  84. package/dist/internal/dom-tables.d.mts.map +1 -0
  85. package/dist/internal/dom-tables.mjs +167 -0
  86. package/dist/internal/dom-tables.mjs.map +1 -0
  87. package/dist/internal/index.d.mts +7 -0
  88. package/dist/internal/index.d.mts.map +1 -0
  89. package/dist/internal/index.mjs +7 -0
  90. package/dist/internal/index.mjs.map +1 -0
  91. package/dist/internal/is-view.d.mts +3 -0
  92. package/dist/internal/is-view.d.mts.map +1 -0
  93. package/dist/internal/is-view.mjs +4 -0
  94. package/dist/internal/is-view.mjs.map +1 -0
  95. package/dist/internal/mountable-tag.d.mts +6 -0
  96. package/dist/internal/mountable-tag.d.mts.map +1 -0
  97. package/dist/internal/mountable-tag.mjs +6 -0
  98. package/dist/internal/mountable-tag.mjs.map +1 -0
  99. package/dist/internal/view-tag.d.mts +7 -0
  100. package/dist/internal/view-tag.d.mts.map +1 -0
  101. package/dist/internal/view-tag.mjs +7 -0
  102. package/dist/internal/view-tag.mjs.map +1 -0
  103. package/dist/jsx/child.d.mts +20 -0
  104. package/dist/jsx/child.d.mts.map +1 -0
  105. package/dist/jsx/child.mjs +33 -0
  106. package/dist/jsx/child.mjs.map +1 -0
  107. package/dist/jsx/element-props.d.mts +78 -0
  108. package/dist/jsx/element-props.d.mts.map +1 -0
  109. package/dist/jsx/element-props.mjs +2 -0
  110. package/dist/jsx/element-props.mjs.map +1 -0
  111. package/dist/jsx/event-prop-names.d.mts +120 -0
  112. package/dist/jsx/event-prop-names.d.mts.map +1 -0
  113. package/dist/jsx/event-prop-names.mjs +2 -0
  114. package/dist/jsx/event-prop-names.mjs.map +1 -0
  115. package/dist/jsx/index.d.mts +7 -0
  116. package/dist/jsx/index.d.mts.map +1 -0
  117. package/dist/jsx/index.mjs +7 -0
  118. package/dist/jsx/index.mjs.map +1 -0
  119. package/dist/jsx/jsx-namespace.d.mts +11 -0
  120. package/dist/jsx/jsx-namespace.d.mts.map +1 -0
  121. package/dist/jsx/jsx-namespace.mjs +2 -0
  122. package/dist/jsx/jsx-namespace.mjs.map +1 -0
  123. package/dist/jsx/jsx.d.mts +18 -0
  124. package/dist/jsx/jsx.d.mts.map +1 -0
  125. package/dist/jsx/jsx.mjs +46 -0
  126. package/dist/jsx/jsx.mjs.map +1 -0
  127. package/dist/jsx/mountable.d.mts +25 -0
  128. package/dist/jsx/mountable.d.mts.map +1 -0
  129. package/dist/jsx/mountable.mjs +7 -0
  130. package/dist/jsx/mountable.mjs.map +1 -0
  131. package/dist/jsx-dev-runtime.d.mts +7 -0
  132. package/dist/jsx-dev-runtime.d.mts.map +1 -0
  133. package/dist/jsx-dev-runtime.mjs +7 -0
  134. package/dist/jsx-dev-runtime.mjs.map +1 -0
  135. package/dist/jsx-runtime.d.mts +7 -0
  136. package/dist/jsx-runtime.d.mts.map +1 -0
  137. package/dist/jsx-runtime.mjs +7 -0
  138. package/dist/jsx-runtime.mjs.map +1 -0
  139. package/dist/render.d.mts +14 -0
  140. package/dist/render.d.mts.map +1 -0
  141. package/dist/render.mjs +25 -0
  142. package/dist/render.mjs.map +1 -0
  143. package/package.json +97 -3
  144. package/src/bind/bind-props.mts +174 -0
  145. package/src/bind/index.mts +1 -0
  146. package/src/constant/bindable.mts +19 -0
  147. package/src/constant/constant.mts +24 -0
  148. package/src/constant/constant.test.mts +70 -0
  149. package/src/constant/derive.mts +51 -0
  150. package/src/constant/derive.test.mts +64 -0
  151. package/src/constant/index.mts +6 -0
  152. package/src/constant/map-bindable.mts +23 -0
  153. package/src/constant/map-bindable.test.mts +63 -0
  154. package/src/constant/to-observable.mts +20 -0
  155. package/src/constant/to-observable.test.mts +37 -0
  156. package/src/constant/view.mts +35 -0
  157. package/src/constant/view.test.mts +40 -0
  158. package/src/control/for.mts +362 -0
  159. package/src/control/if.mts +28 -0
  160. package/src/control/index.mts +5 -0
  161. package/src/control/longest-increasing-subsequence.mts +59 -0
  162. package/src/control/longest-increasing-subsequence.test.mts +26 -0
  163. package/src/control/match-optional.mts +41 -0
  164. package/src/control/match-result.mts +48 -0
  165. package/src/control/mount-switch.mts +75 -0
  166. package/src/control/portal.mts +32 -0
  167. package/src/index.mts +4 -0
  168. package/src/internal/as-constant.mts +11 -0
  169. package/src/internal/bind-value.mts +58 -0
  170. package/src/internal/dom-tables.mts +176 -0
  171. package/src/internal/index.mts +6 -0
  172. package/src/internal/is-view.mts +6 -0
  173. package/src/internal/mountable-tag.mts +5 -0
  174. package/src/internal/view-tag.mts +6 -0
  175. package/src/jsx/child.mts +51 -0
  176. package/src/jsx/element-props.mts +132 -0
  177. package/src/jsx/element-props.test.mts +28 -0
  178. package/src/jsx/event-prop-names.mts +119 -0
  179. package/src/jsx/index.mts +6 -0
  180. package/src/jsx/jsx-namespace.mts +11 -0
  181. package/src/jsx/jsx.mts +63 -0
  182. package/src/jsx/mountable.mts +31 -0
  183. package/src/jsx-dev-runtime.mts +6 -0
  184. package/src/jsx-runtime.mts +11 -0
  185. package/src/render.mts +32 -0
@@ -0,0 +1,58 @@
1
+ import {
2
+ isObservable,
3
+ onScopeDispose,
4
+ type InitializedObservable,
5
+ } from 'synstate';
6
+ import type { Bindable } from '../constant/index.mjs';
7
+ import { isView } from './is-view.mjs';
8
+
9
+ /**
10
+ * Writes the value of a {@link Bindable} with `write`, and again on every
11
+ * update (SPEC.md 7.1).
12
+ *
13
+ * - A constant is written once.
14
+ * - A completed observable has its current value written once.
15
+ * - Any other observable is subscribed to, and the subscription is released
16
+ * when the current scope is disposed.
17
+ *
18
+ * A view is the same as its source, with its function applied before `write`.
19
+ */
20
+ export const bindValue = <T,>(
21
+ bindable: Bindable<T>,
22
+ write: (value: T) => void,
23
+ ): void => {
24
+ if (isView(bindable)) {
25
+ bindable.open((source, fn) => {
26
+ subscribeInScope(source, (value) => {
27
+ write(fn(value));
28
+ });
29
+ });
30
+
31
+ return;
32
+ }
33
+
34
+ if (isObservable(bindable)) {
35
+ subscribeInScope(bindable, write);
36
+
37
+ return;
38
+ }
39
+
40
+ write(bindable);
41
+ };
42
+
43
+ const subscribeInScope = <A,>(
44
+ source: InitializedObservable<A>,
45
+ write: (value: A) => void,
46
+ ): void => {
47
+ if (source.isCompleted) {
48
+ write(source.getSnapshot().value);
49
+
50
+ return;
51
+ }
52
+
53
+ const subscription = source.subscribe(write);
54
+
55
+ onScopeDispose(() => {
56
+ subscription.unsubscribe();
57
+ });
58
+ };
@@ -0,0 +1,176 @@
1
+ import type { ReadonlyRecord, RelaxedExclude } from 'ts-type-forge';
2
+
3
+ /**
4
+ * The keys written as DOM properties (`element[key] = value`) rather than as
5
+ * attributes. These are the ones a user's input moves away from the attribute
6
+ * (`value`, `checked`, …), and the enumerated attributes whose `false` is a
7
+ * value rather than an absence (`draggable`, `spellcheck`, `translate`).
8
+ *
9
+ * The props types and the binding both read this table (SPEC.md 5.2).
10
+ */
11
+ export const propertyNames = [
12
+ 'checked',
13
+ 'defaultChecked',
14
+ 'defaultValue',
15
+ 'draggable',
16
+ 'indeterminate',
17
+ 'muted',
18
+ 'selected',
19
+ 'spellcheck',
20
+ 'translate',
21
+ 'value',
22
+ ] as const;
23
+
24
+ /**
25
+ * DOM property names whose attribute is spelt differently. Every other name is
26
+ * passed to `setAttribute`, which lower-cases it for an HTML element
27
+ * (`tabIndex` becomes `tabindex`).
28
+ */
29
+ export const attributeNameOf: ReadonlyRecord<string, string> = {
30
+ acceptCharset: 'accept-charset',
31
+ htmlFor: 'for',
32
+ httpEquiv: 'http-equiv',
33
+ } as const;
34
+
35
+ /**
36
+ * The tags created in the SVG namespace: those of `SVGElementTagNameMap` that
37
+ * HTML does not also have. `a`, `script`, `style` and `title` are created as
38
+ * HTML elements wherever they appear.
39
+ */
40
+ export const svgTagNames = [
41
+ 'animate',
42
+ 'animateMotion',
43
+ 'animateTransform',
44
+ 'circle',
45
+ 'clipPath',
46
+ 'defs',
47
+ 'desc',
48
+ 'ellipse',
49
+ 'feBlend',
50
+ 'feColorMatrix',
51
+ 'feComponentTransfer',
52
+ 'feComposite',
53
+ 'feConvolveMatrix',
54
+ 'feDiffuseLighting',
55
+ 'feDisplacementMap',
56
+ 'feDistantLight',
57
+ 'feDropShadow',
58
+ 'feFlood',
59
+ 'feFuncA',
60
+ 'feFuncB',
61
+ 'feFuncG',
62
+ 'feFuncR',
63
+ 'feGaussianBlur',
64
+ 'feImage',
65
+ 'feMerge',
66
+ 'feMergeNode',
67
+ 'feMorphology',
68
+ 'feOffset',
69
+ 'fePointLight',
70
+ 'feSpecularLighting',
71
+ 'feSpotLight',
72
+ 'feTile',
73
+ 'feTurbulence',
74
+ 'filter',
75
+ 'foreignObject',
76
+ 'g',
77
+ 'image',
78
+ 'line',
79
+ 'linearGradient',
80
+ 'marker',
81
+ 'mask',
82
+ 'metadata',
83
+ 'mpath',
84
+ 'path',
85
+ 'pattern',
86
+ 'polygon',
87
+ 'polyline',
88
+ 'radialGradient',
89
+ 'rect',
90
+ 'set',
91
+ 'stop',
92
+ 'svg',
93
+ 'switch',
94
+ 'symbol',
95
+ 'text',
96
+ 'textPath',
97
+ 'tspan',
98
+ 'use',
99
+ 'view',
100
+ ] as const satisfies readonly RelaxedExclude<
101
+ keyof SVGElementTagNameMap,
102
+ keyof HTMLElementTagNameMap
103
+ >[];
104
+
105
+ export type SvgTagName = (typeof svgTagNames)[number];
106
+
107
+ /**
108
+ * The attributes an SVG element takes. SVG properties are animated values
109
+ * (`SVGAnimatedLength`), so unlike HTML they cannot be read off the element
110
+ * types; these are listed by hand and written with `setAttribute` as named.
111
+ */
112
+ export const svgAttributeNames = [
113
+ 'clip-path',
114
+ 'clip-rule',
115
+ 'cx',
116
+ 'cy',
117
+ 'd',
118
+ 'dominant-baseline',
119
+ 'dx',
120
+ 'dy',
121
+ 'fill',
122
+ 'fill-opacity',
123
+ 'fill-rule',
124
+ 'filter',
125
+ 'font-family',
126
+ 'font-size',
127
+ 'font-weight',
128
+ 'fx',
129
+ 'fy',
130
+ 'gradientTransform',
131
+ 'gradientUnits',
132
+ 'height',
133
+ 'href',
134
+ 'id',
135
+ 'marker-end',
136
+ 'marker-mid',
137
+ 'marker-start',
138
+ 'markerHeight',
139
+ 'markerUnits',
140
+ 'markerWidth',
141
+ 'mask',
142
+ 'offset',
143
+ 'opacity',
144
+ 'orient',
145
+ 'pathLength',
146
+ 'patternUnits',
147
+ 'points',
148
+ 'preserveAspectRatio',
149
+ 'r',
150
+ 'refX',
151
+ 'refY',
152
+ 'role',
153
+ 'rx',
154
+ 'ry',
155
+ 'stop-color',
156
+ 'stop-opacity',
157
+ 'stroke',
158
+ 'stroke-dasharray',
159
+ 'stroke-dashoffset',
160
+ 'stroke-linecap',
161
+ 'stroke-linejoin',
162
+ 'stroke-opacity',
163
+ 'stroke-width',
164
+ 'tabindex',
165
+ 'text-anchor',
166
+ 'transform',
167
+ 'viewBox',
168
+ 'visibility',
169
+ 'width',
170
+ 'x',
171
+ 'x1',
172
+ 'x2',
173
+ 'y',
174
+ 'y1',
175
+ 'y2',
176
+ ] as const;
@@ -0,0 +1,6 @@
1
+ export * from './as-constant.mjs';
2
+ export * from './bind-value.mjs';
3
+ export * from './dom-tables.mjs';
4
+ export * from './is-view.mjs';
5
+ export * from './mountable-tag.mjs';
6
+ export * from './view-tag.mjs';
@@ -0,0 +1,6 @@
1
+ import { hasKey, isRecord } from 'ts-data-forge';
2
+ import type { Bindable, View } from '../constant/index.mjs';
3
+ import { viewTag } from './view-tag.mjs';
4
+
5
+ export const isView = <T,>(bindable: Bindable<T>): bindable is View<T> =>
6
+ isRecord(bindable) && hasKey(bindable, viewTag);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The key a {@link Mountable} carries, so that mounting a child can tell a
3
+ * description from a `Bindable` at run time.
4
+ */
5
+ export const mountableTag: unique symbol = Symbol.for('synstate-dom/mountable');
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The key a {@link View} carries, so that binding can tell it from a constant
3
+ * at run time. `Symbol.for` rather than `Symbol`, matching the mark synstate
4
+ * puts on its observables: two copies of the module still agree.
5
+ */
6
+ export const viewTag: unique symbol = Symbol.for('synstate-dom/view');
@@ -0,0 +1,51 @@
1
+ import { isObservable } from 'synstate';
2
+ import { Arr, hasKey, isNumber, isRecord, isString } from 'ts-data-forge';
3
+ import type { Bindable } from '../constant/index.mjs';
4
+ import { bindValue, mountableTag, viewTag } from '../internal/index.mjs';
5
+ import type { Mountable, MountedNodes } from './mountable.mjs';
6
+
7
+ /**
8
+ * What a JSX element accepts as a child. Text must be a `Bindable` like any
9
+ * other data: `<p>{constant('big')}</p>`, never `<p>big</p>` (SPEC.md 5.1).
10
+ */
11
+ export type Child = Bindable<number | string> | Mountable | readonly Child[];
12
+
13
+ /**
14
+ * Mounts a child in the current scope and returns its nodes in order: a
15
+ * description is mounted, a `Bindable` becomes a text node bound to it, and an
16
+ * array is flattened.
17
+ */
18
+ export const mountChild = (child: Child): readonly ChildNode[] => {
19
+ if (Arr.isArray(child)) {
20
+ return child.flatMap(mountChild);
21
+ }
22
+
23
+ if (isMountable(child)) {
24
+ return toNodes(child.mount());
25
+ }
26
+
27
+ const mut_text = document.createTextNode('');
28
+
29
+ bindValue(child, (value) => {
30
+ mut_text.data = value.toString();
31
+ });
32
+
33
+ return [mut_text];
34
+ };
35
+
36
+ export const toNodes = (nodes: MountedNodes): readonly ChildNode[] =>
37
+ Arr.isArray(nodes) ? nodes : ([nodes] as const);
38
+
39
+ /**
40
+ * Tells a child from any other value, one level deep: the elements of an
41
+ * array are checked as they are mounted.
42
+ */
43
+ export const isChild = (value: unknown): value is Child =>
44
+ Arr.isArray(value) ||
45
+ isString(value) ||
46
+ isNumber(value) ||
47
+ isObservable(value) ||
48
+ (isRecord(value) && (hasKey(value, mountableTag) || hasKey(value, viewTag)));
49
+
50
+ const isMountable = (child: Child): child is Mountable =>
51
+ isRecord(child) && hasKey(child, mountableTag);
@@ -0,0 +1,132 @@
1
+ import type { ReadonlyRecord, StrictPick, TypeEq } from 'ts-type-forge';
2
+ import type { Bindable } from '../constant/index.mjs';
3
+ import type {
4
+ propertyNames,
5
+ svgAttributeNames,
6
+ SvgTagName,
7
+ } from '../internal/index.mjs';
8
+ import type { Child } from './child.mjs';
9
+ import type { EventPropNames } from './event-prop-names.mjs';
10
+
11
+ /**
12
+ * The props every intrinsic element takes.
13
+ *
14
+ * - `ref` is called once with the element, right after it is created and
15
+ * before its children are mounted or anything is bound.
16
+ * - `class` is either a string or an object of `Bindable<boolean>`s, one per
17
+ * class name, never both (SPEC.md 7.2).
18
+ * - `style` is either a `cssText` string or an object of CSS properties, each
19
+ * bound with `style.setProperty`, so the keys are the hyphenated names
20
+ * (`'background-color'`, `'--accent'`).
21
+ */
22
+ export type CommonElementProps<E extends Element> = Readonly<{
23
+ ref?: (element: E) => void;
24
+ children?: Child;
25
+ class?: Bindable<string> | ClassObject;
26
+ style?: Bindable<string> | StyleObject;
27
+ }> &
28
+ Partial<
29
+ ReadonlyRecord<
30
+ `aria-${string}` | `data-${string}`,
31
+ Bindable<string | undefined>
32
+ >
33
+ >;
34
+
35
+ /** `class` as one `Bindable<boolean>` per class name. */
36
+ export type ClassObject = ReadonlyRecord<string, Bindable<boolean>>;
37
+
38
+ /**
39
+ * `style` as one `Bindable<string | undefined>` per hyphenated CSS property
40
+ * name; `undefined` removes the property.
41
+ */
42
+ export type StyleObject = ReadonlyRecord<string, Bindable<string | undefined>>;
43
+
44
+ /**
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`.
48
+ *
49
+ * An attribute, `aria-*` and `data-*` included, also takes `undefined`, which
50
+ * removes it: a value that comes and goes is an observable of `T | undefined`
51
+ * (SPEC.md 4.1). A DOM property of `propertyNames` (`value`, `checked`, …) has
52
+ * no absent state and does not.
53
+ */
54
+ export type HtmlElementProps<E extends HTMLElement> = CommonElementProps<E> &
55
+ ReflectedProps<E> &
56
+ EventProps<E, HTMLElementEventMap>;
57
+
58
+ /**
59
+ * The props of an SVG element: the common ones, the attributes of
60
+ * `svgAttributeNames`, and one handler per event of `SVGElementEventMap`.
61
+ */
62
+ export type SvgElementProps<E extends SVGElement> = CommonElementProps<E> &
63
+ Partial<
64
+ ReadonlyRecord<
65
+ (typeof svgAttributeNames)[number],
66
+ Bindable<number | string | undefined>
67
+ >
68
+ > &
69
+ EventProps<E, SVGElementEventMap>;
70
+
71
+ /**
72
+ * One handler per event, named `on` followed by the event's name in
73
+ * PascalCase (`onClick`, `onPointerDown`). `currentTarget` is narrowed to the
74
+ * element. A handler is a function, never an observable.
75
+ */
76
+ export type EventProps<E extends Element, M> = Readonly<{
77
+ [K in keyof M & keyof EventPropNames as EventPropNames[K]]?: (
78
+ event: M[K] & Readonly<{ currentTarget: E }>,
79
+ ) => void;
80
+ }>;
81
+
82
+ export type GeneratedIntrinsicElements = Readonly<{
83
+ [K in keyof HTMLElementTagNameMap]: HtmlElementProps<
84
+ HTMLElementTagNameMap[K]
85
+ >;
86
+ }> &
87
+ Readonly<{ [K in SvgTagName]: SvgElementProps<SVGElementTagNameMap[K]> }>;
88
+
89
+ type ReflectedProps<E> = Readonly<{
90
+ [K in keyof E as ReflectedKey<E, K>]?: Bindable<
91
+ K extends PropertyName ? NonNullable<E[K]> : NonNullable<E[K]> | undefined
92
+ >;
93
+ }>;
94
+
95
+ type PropertyName = (typeof propertyNames)[number];
96
+
97
+ /**
98
+ * `K` if it is a writable primitive property of `E`, and `never` otherwise.
99
+ * An index signature is dropped: `HTMLFormElement` has one for its named
100
+ * controls, which would otherwise swallow every other key.
101
+ */
102
+ type ReflectedKey<E, K extends keyof E> = string extends K
103
+ ? never
104
+ : number extends K
105
+ ? never
106
+ : K extends ExcludedKey | `aria${string}` | `on${string}`
107
+ ? never
108
+ : NonNullable<E[K]> extends boolean | number | string
109
+ ? IsWritable<E, K> extends true
110
+ ? K
111
+ : never
112
+ : never;
113
+
114
+ type IsWritable<E, K extends keyof E> =
115
+ TypeEq<StrictPick<E, K>, Readonly<StrictPick<E, K>>> extends true
116
+ ? false
117
+ : true;
118
+
119
+ /**
120
+ * Writable primitive properties that are not attributes, or that are props of
121
+ * their own (`class`, `style`).
122
+ */
123
+ type ExcludedKey =
124
+ | 'className'
125
+ | 'innerHTML'
126
+ | 'innerText'
127
+ | 'nodeValue'
128
+ | 'outerHTML'
129
+ | 'outerText'
130
+ | 'scrollLeft'
131
+ | 'scrollTop'
132
+ | 'textContent';
@@ -0,0 +1,28 @@
1
+ import { expectType } from 'ts-data-forge';
2
+ import type { EventPropNames } from './event-prop-names.mjs';
3
+
4
+ describe('EventPropNames', () => {
5
+ test('names every event of HTML and SVG elements', () => {
6
+ expectType<
7
+ keyof EventPropNames,
8
+ keyof HTMLElementEventMap | keyof SVGElementEventMap
9
+ >('=');
10
+
11
+ assert.isTrue(true);
12
+ });
13
+
14
+ test('lower-casing a prop name gives its event name back', () => {
15
+ expectType<
16
+ {
17
+ [K in keyof EventPropNames]: Lowercase<
18
+ EventPropNames[K]
19
+ > extends `on${K}`
20
+ ? true
21
+ : false;
22
+ }[keyof EventPropNames],
23
+ true
24
+ >('=');
25
+
26
+ assert.isTrue(true);
27
+ });
28
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The prop name of each DOM event, in the PascalCase React uses
3
+ * (`pointerdown` → `onPointerDown`). The event names are single lower-case
4
+ * words, so the word boundaries have to be written down; binding needs no
5
+ * table, since lower-casing the prop name gives the event name back.
6
+ *
7
+ * Covers every key of `HTMLElementEventMap` and `SVGElementEventMap`, which
8
+ * `element-props.test.mts` asserts.
9
+ */
10
+ export type EventPropNames = Readonly<{
11
+ abort: 'onAbort';
12
+ animationcancel: 'onAnimationCancel';
13
+ animationend: 'onAnimationEnd';
14
+ animationiteration: 'onAnimationIteration';
15
+ animationstart: 'onAnimationStart';
16
+ auxclick: 'onAuxClick';
17
+ beforeinput: 'onBeforeInput';
18
+ beforematch: 'onBeforeMatch';
19
+ beforetoggle: 'onBeforeToggle';
20
+ blur: 'onBlur';
21
+ cancel: 'onCancel';
22
+ canplay: 'onCanPlay';
23
+ canplaythrough: 'onCanPlayThrough';
24
+ change: 'onChange';
25
+ click: 'onClick';
26
+ close: 'onClose';
27
+ command: 'onCommand';
28
+ compositionend: 'onCompositionEnd';
29
+ compositionstart: 'onCompositionStart';
30
+ compositionupdate: 'onCompositionUpdate';
31
+ contextlost: 'onContextLost';
32
+ contextmenu: 'onContextMenu';
33
+ contextrestored: 'onContextRestored';
34
+ copy: 'onCopy';
35
+ cuechange: 'onCueChange';
36
+ cut: 'onCut';
37
+ dblclick: 'onDblClick';
38
+ drag: 'onDrag';
39
+ dragend: 'onDragEnd';
40
+ dragenter: 'onDragEnter';
41
+ dragleave: 'onDragLeave';
42
+ dragover: 'onDragOver';
43
+ dragstart: 'onDragStart';
44
+ drop: 'onDrop';
45
+ durationchange: 'onDurationChange';
46
+ emptied: 'onEmptied';
47
+ ended: 'onEnded';
48
+ error: 'onError';
49
+ focus: 'onFocus';
50
+ focusin: 'onFocusIn';
51
+ focusout: 'onFocusOut';
52
+ formdata: 'onFormData';
53
+ fullscreenchange: 'onFullscreenChange';
54
+ fullscreenerror: 'onFullscreenError';
55
+ gotpointercapture: 'onGotPointerCapture';
56
+ input: 'onInput';
57
+ invalid: 'onInvalid';
58
+ keydown: 'onKeyDown';
59
+ keypress: 'onKeyPress';
60
+ keyup: 'onKeyUp';
61
+ load: 'onLoad';
62
+ loadeddata: 'onLoadedData';
63
+ loadedmetadata: 'onLoadedMetadata';
64
+ loadstart: 'onLoadStart';
65
+ lostpointercapture: 'onLostPointerCapture';
66
+ mousedown: 'onMouseDown';
67
+ mouseenter: 'onMouseEnter';
68
+ mouseleave: 'onMouseLeave';
69
+ mousemove: 'onMouseMove';
70
+ mouseout: 'onMouseOut';
71
+ mouseover: 'onMouseOver';
72
+ mouseup: 'onMouseUp';
73
+ paste: 'onPaste';
74
+ pause: 'onPause';
75
+ play: 'onPlay';
76
+ playing: 'onPlaying';
77
+ pointercancel: 'onPointerCancel';
78
+ pointerdown: 'onPointerDown';
79
+ pointerenter: 'onPointerEnter';
80
+ pointerleave: 'onPointerLeave';
81
+ pointermove: 'onPointerMove';
82
+ pointerout: 'onPointerOut';
83
+ pointerover: 'onPointerOver';
84
+ pointerrawupdate: 'onPointerRawUpdate';
85
+ pointerup: 'onPointerUp';
86
+ progress: 'onProgress';
87
+ ratechange: 'onRateChange';
88
+ reset: 'onReset';
89
+ resize: 'onResize';
90
+ scroll: 'onScroll';
91
+ scrollend: 'onScrollEnd';
92
+ securitypolicyviolation: 'onSecurityPolicyViolation';
93
+ seeked: 'onSeeked';
94
+ seeking: 'onSeeking';
95
+ select: 'onSelect';
96
+ selectionchange: 'onSelectionChange';
97
+ selectstart: 'onSelectStart';
98
+ slotchange: 'onSlotChange';
99
+ stalled: 'onStalled';
100
+ submit: 'onSubmit';
101
+ suspend: 'onSuspend';
102
+ timeupdate: 'onTimeUpdate';
103
+ toggle: 'onToggle';
104
+ touchcancel: 'onTouchCancel';
105
+ touchend: 'onTouchEnd';
106
+ touchmove: 'onTouchMove';
107
+ touchstart: 'onTouchStart';
108
+ transitioncancel: 'onTransitionCancel';
109
+ transitionend: 'onTransitionEnd';
110
+ transitionrun: 'onTransitionRun';
111
+ transitionstart: 'onTransitionStart';
112
+ volumechange: 'onVolumeChange';
113
+ waiting: 'onWaiting';
114
+ webkitanimationend: 'onWebkitAnimationEnd';
115
+ webkitanimationiteration: 'onWebkitAnimationIteration';
116
+ webkitanimationstart: 'onWebkitAnimationStart';
117
+ webkittransitionend: 'onWebkitTransitionEnd';
118
+ wheel: 'onWheel';
119
+ }>;
@@ -0,0 +1,6 @@
1
+ export * from './child.mjs';
2
+ export * from './element-props.mjs';
3
+ export * from './event-prop-names.mjs';
4
+ export * from './jsx-namespace.mjs';
5
+ export * from './jsx.mjs';
6
+ export * from './mountable.mjs';
@@ -0,0 +1,11 @@
1
+ import type { GeneratedIntrinsicElements } from './element-props.mjs';
2
+ import type { Mountable } from './mountable.mjs';
3
+
4
+ /** The `JSX` namespace TypeScript reads through `jsxImportSource`. */
5
+ export declare namespace JSX {
6
+ type Element = Mountable;
7
+
8
+ type ElementChildrenAttribute = Readonly<{ children: unknown }>;
9
+
10
+ type IntrinsicElements = GeneratedIntrinsicElements;
11
+ }
@@ -0,0 +1,63 @@
1
+ import { hasKey, isRecord } from 'ts-data-forge';
2
+ import { bindProps } from '../bind/index.mjs';
3
+ import { svgTagNames } from '../internal/index.mjs';
4
+ import { isChild, mountChild, type Child } from './child.mjs';
5
+ import { mountable, type Mountable } from './mountable.mjs';
6
+
7
+ /**
8
+ * The JSX factory (SPEC.md 6.2), exported from `synstate-dom/jsx-runtime` as
9
+ * `jsx` and `jsxs`. Returns a description and does nothing else:
10
+ * no DOM is created and no component is called until it is mounted.
11
+ *
12
+ * TypeScript checks a JSX expression against the `JSX` namespace, not against
13
+ * this signature, so the props of an intrinsic element arrive as `P` and are
14
+ * read with run-time checks.
15
+ *
16
+ */
17
+ export const createMountable = <P,>(
18
+ type: string | ((componentProps: P) => Mountable),
19
+ props: P,
20
+ key?: unknown,
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.
24
+ const propsWithKey = key === undefined ? props : ({ ...props, key } as const);
25
+
26
+ return typeof type === 'string'
27
+ ? mountable(() => mountElement(type, props))
28
+ : mountable(() => type(propsWithKey).mount());
29
+ };
30
+
31
+ /** Mounts its children in order, with no element around them. */
32
+ export const Fragment = (props: Readonly<{ children?: Child }>): Mountable =>
33
+ mountable(() => mountChild(props.children ?? []));
34
+
35
+ const mountElement = (tag: string, props: unknown): Element => {
36
+ const element = svgTagNameSet.has(tag)
37
+ ? document.createElementNS(svgNamespace, tag)
38
+ : document.createElement(tag);
39
+
40
+ if (!isRecord(props)) {
41
+ return element;
42
+ }
43
+
44
+ if (hasKey(props, 'ref') && isRefCallback(props.ref)) {
45
+ props.ref(element);
46
+ }
47
+
48
+ // Children first, so that a `<select>`'s `value` finds its `<option>`s.
49
+ if (hasKey(props, 'children') && isChild(props.children)) {
50
+ element.append(...mountChild(props.children));
51
+ }
52
+
53
+ bindProps(element, props);
54
+
55
+ return element;
56
+ };
57
+
58
+ const svgNamespace = 'http://www.w3.org/2000/svg';
59
+
60
+ const svgTagNameSet: ReadonlySet<string> = new Set(svgTagNames);
61
+
62
+ const isRefCallback = (value: unknown): value is (element: Element) => void =>
63
+ typeof value === 'function';