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.
- package/LICENSE +201 -0
- package/README.md +218 -2
- package/dist/bind/bind-props.d.mts +9 -0
- package/dist/bind/bind-props.d.mts.map +1 -0
- package/dist/bind/bind-props.mjs +117 -0
- package/dist/bind/bind-props.mjs.map +1 -0
- package/dist/bind/index.d.mts +2 -0
- package/dist/bind/index.d.mts.map +1 -0
- package/dist/bind/index.mjs +2 -0
- package/dist/bind/index.mjs.map +1 -0
- package/dist/constant/bindable.d.mts +11 -0
- package/dist/constant/bindable.d.mts.map +1 -0
- package/dist/constant/bindable.mjs +2 -0
- package/dist/constant/bindable.mjs.map +1 -0
- package/dist/constant/constant.d.mts +20 -0
- package/dist/constant/constant.d.mts.map +1 -0
- package/dist/constant/constant.mjs +10 -0
- package/dist/constant/constant.mjs.map +1 -0
- package/dist/constant/derive.d.mts +16 -0
- package/dist/constant/derive.d.mts.map +1 -0
- package/dist/constant/derive.mjs +28 -0
- package/dist/constant/derive.mjs.map +1 -0
- package/dist/constant/index.d.mts +7 -0
- package/dist/constant/index.d.mts.map +1 -0
- package/dist/constant/index.mjs +7 -0
- package/dist/constant/index.mjs.map +1 -0
- package/dist/constant/map-bindable.d.mts +10 -0
- package/dist/constant/map-bindable.d.mts.map +1 -0
- package/dist/constant/map-bindable.mjs +16 -0
- package/dist/constant/map-bindable.mjs.map +1 -0
- package/dist/constant/to-observable.d.mts +12 -0
- package/dist/constant/to-observable.d.mts.map +1 -0
- package/dist/constant/to-observable.mjs +16 -0
- package/dist/constant/to-observable.mjs.map +1 -0
- package/dist/constant/view.d.mts +25 -0
- package/dist/constant/view.d.mts.map +1 -0
- package/dist/constant/view.mjs +15 -0
- package/dist/constant/view.mjs.map +1 -0
- package/dist/control/for.d.mts +29 -0
- package/dist/control/for.d.mts.map +1 -0
- package/dist/control/for.mjs +201 -0
- package/dist/control/for.mjs.map +1 -0
- package/dist/control/if.d.mts +16 -0
- package/dist/control/if.d.mts.map +1 -0
- package/dist/control/if.mjs +12 -0
- package/dist/control/if.mjs.map +1 -0
- package/dist/control/index.d.mts +6 -0
- package/dist/control/index.d.mts.map +1 -0
- package/dist/control/index.mjs +6 -0
- package/dist/control/index.mjs.map +1 -0
- package/dist/control/longest-increasing-subsequence.d.mts +7 -0
- package/dist/control/longest-increasing-subsequence.d.mts.map +1 -0
- package/dist/control/longest-increasing-subsequence.mjs +37 -0
- package/dist/control/longest-increasing-subsequence.mjs.map +1 -0
- package/dist/control/match-optional.d.mts +19 -0
- package/dist/control/match-optional.d.mts.map +1 -0
- package/dist/control/match-optional.mjs +21 -0
- package/dist/control/match-optional.mjs.map +1 -0
- package/dist/control/match-result.d.mts +19 -0
- package/dist/control/match-result.d.mts.map +1 -0
- package/dist/control/match-result.mjs +27 -0
- package/dist/control/match-result.mjs.map +1 -0
- package/dist/control/mount-switch.d.mts +18 -0
- package/dist/control/mount-switch.d.mts.map +1 -0
- package/dist/control/mount-switch.mjs +51 -0
- package/dist/control/mount-switch.mjs.map +1 -0
- package/dist/control/portal.d.mts +15 -0
- package/dist/control/portal.d.mts.map +1 -0
- package/dist/control/portal.mjs +22 -0
- package/dist/control/portal.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal/as-constant.d.mts +9 -0
- package/dist/internal/as-constant.d.mts.map +1 -0
- package/dist/internal/as-constant.mjs +10 -0
- package/dist/internal/as-constant.mjs.map +1 -0
- package/dist/internal/bind-value.d.mts +14 -0
- package/dist/internal/bind-value.d.mts.map +1 -0
- package/dist/internal/bind-value.mjs +39 -0
- package/dist/internal/bind-value.mjs.map +1 -0
- package/dist/internal/dom-tables.d.mts +30 -0
- package/dist/internal/dom-tables.d.mts.map +1 -0
- package/dist/internal/dom-tables.mjs +167 -0
- package/dist/internal/dom-tables.mjs.map +1 -0
- package/dist/internal/index.d.mts +7 -0
- package/dist/internal/index.d.mts.map +1 -0
- package/dist/internal/index.mjs +7 -0
- package/dist/internal/index.mjs.map +1 -0
- package/dist/internal/is-view.d.mts +3 -0
- package/dist/internal/is-view.d.mts.map +1 -0
- package/dist/internal/is-view.mjs +4 -0
- package/dist/internal/is-view.mjs.map +1 -0
- package/dist/internal/mountable-tag.d.mts +6 -0
- package/dist/internal/mountable-tag.d.mts.map +1 -0
- package/dist/internal/mountable-tag.mjs +6 -0
- package/dist/internal/mountable-tag.mjs.map +1 -0
- package/dist/internal/view-tag.d.mts +7 -0
- package/dist/internal/view-tag.d.mts.map +1 -0
- package/dist/internal/view-tag.mjs +7 -0
- package/dist/internal/view-tag.mjs.map +1 -0
- package/dist/jsx/child.d.mts +20 -0
- package/dist/jsx/child.d.mts.map +1 -0
- package/dist/jsx/child.mjs +33 -0
- package/dist/jsx/child.mjs.map +1 -0
- package/dist/jsx/element-props.d.mts +78 -0
- package/dist/jsx/element-props.d.mts.map +1 -0
- package/dist/jsx/element-props.mjs +2 -0
- package/dist/jsx/element-props.mjs.map +1 -0
- package/dist/jsx/event-prop-names.d.mts +120 -0
- package/dist/jsx/event-prop-names.d.mts.map +1 -0
- package/dist/jsx/event-prop-names.mjs +2 -0
- package/dist/jsx/event-prop-names.mjs.map +1 -0
- package/dist/jsx/index.d.mts +7 -0
- package/dist/jsx/index.d.mts.map +1 -0
- package/dist/jsx/index.mjs +7 -0
- package/dist/jsx/index.mjs.map +1 -0
- package/dist/jsx/jsx-namespace.d.mts +11 -0
- package/dist/jsx/jsx-namespace.d.mts.map +1 -0
- package/dist/jsx/jsx-namespace.mjs +2 -0
- package/dist/jsx/jsx-namespace.mjs.map +1 -0
- package/dist/jsx/jsx.d.mts +18 -0
- package/dist/jsx/jsx.d.mts.map +1 -0
- package/dist/jsx/jsx.mjs +46 -0
- package/dist/jsx/jsx.mjs.map +1 -0
- package/dist/jsx/mountable.d.mts +25 -0
- package/dist/jsx/mountable.d.mts.map +1 -0
- package/dist/jsx/mountable.mjs +7 -0
- package/dist/jsx/mountable.mjs.map +1 -0
- package/dist/jsx-dev-runtime.d.mts +7 -0
- package/dist/jsx-dev-runtime.d.mts.map +1 -0
- package/dist/jsx-dev-runtime.mjs +7 -0
- package/dist/jsx-dev-runtime.mjs.map +1 -0
- package/dist/jsx-runtime.d.mts +7 -0
- package/dist/jsx-runtime.d.mts.map +1 -0
- package/dist/jsx-runtime.mjs +7 -0
- package/dist/jsx-runtime.mjs.map +1 -0
- package/dist/render.d.mts +14 -0
- package/dist/render.d.mts.map +1 -0
- package/dist/render.mjs +25 -0
- package/dist/render.mjs.map +1 -0
- package/package.json +97 -3
- package/src/bind/bind-props.mts +174 -0
- package/src/bind/index.mts +1 -0
- package/src/constant/bindable.mts +19 -0
- package/src/constant/constant.mts +24 -0
- package/src/constant/constant.test.mts +70 -0
- package/src/constant/derive.mts +51 -0
- package/src/constant/derive.test.mts +64 -0
- package/src/constant/index.mts +6 -0
- package/src/constant/map-bindable.mts +23 -0
- package/src/constant/map-bindable.test.mts +63 -0
- package/src/constant/to-observable.mts +20 -0
- package/src/constant/to-observable.test.mts +37 -0
- package/src/constant/view.mts +35 -0
- package/src/constant/view.test.mts +40 -0
- package/src/control/for.mts +362 -0
- package/src/control/if.mts +28 -0
- package/src/control/index.mts +5 -0
- package/src/control/longest-increasing-subsequence.mts +59 -0
- package/src/control/longest-increasing-subsequence.test.mts +26 -0
- package/src/control/match-optional.mts +41 -0
- package/src/control/match-result.mts +48 -0
- package/src/control/mount-switch.mts +75 -0
- package/src/control/portal.mts +32 -0
- package/src/index.mts +4 -0
- package/src/internal/as-constant.mts +11 -0
- package/src/internal/bind-value.mts +58 -0
- package/src/internal/dom-tables.mts +176 -0
- package/src/internal/index.mts +6 -0
- package/src/internal/is-view.mts +6 -0
- package/src/internal/mountable-tag.mts +5 -0
- package/src/internal/view-tag.mts +6 -0
- package/src/jsx/child.mts +51 -0
- package/src/jsx/element-props.mts +132 -0
- package/src/jsx/element-props.test.mts +28 -0
- package/src/jsx/event-prop-names.mts +119 -0
- package/src/jsx/index.mts +6 -0
- package/src/jsx/jsx-namespace.mts +11 -0
- package/src/jsx/jsx.mts +63 -0
- package/src/jsx/mountable.mts +31 -0
- package/src/jsx-dev-runtime.mts +6 -0
- package/src/jsx-runtime.mts +11 -0
- 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
|
+
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,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,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
|
+
}
|
package/src/jsx/jsx.mts
ADDED
|
@@ -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';
|