@bquery/bquery 1.4.0 → 1.6.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 +586 -527
- package/dist/component/component.d.ts +13 -5
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/html.d.ts +40 -3
- package/dist/component/html.d.ts.map +1 -1
- package/dist/component/index.d.ts +4 -2
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/library.d.ts +34 -0
- package/dist/component/library.d.ts.map +1 -0
- package/dist/component/types.d.ts +132 -13
- package/dist/component/types.d.ts.map +1 -1
- package/dist/component-BEQgt5hl.js +600 -0
- package/dist/component-BEQgt5hl.js.map +1 -0
- package/dist/component.es.mjs +7 -184
- package/dist/config-DRmZZno3.js +40 -0
- package/dist/config-DRmZZno3.js.map +1 -0
- package/dist/core-BGQJVw0-.js +35 -0
- package/dist/core-BGQJVw0-.js.map +1 -0
- package/dist/core-CCEabVHl.js +648 -0
- package/dist/core-CCEabVHl.js.map +1 -0
- package/dist/core.es.mjs +45 -1261
- package/dist/effect-AFRW_Plg.js +84 -0
- package/dist/effect-AFRW_Plg.js.map +1 -0
- package/dist/full.d.ts +8 -8
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +101 -91
- package/dist/full.iife.js +173 -3
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +173 -3
- package/dist/full.umd.js.map +1 -1
- package/dist/index.es.mjs +147 -139
- package/dist/motion/transition.d.ts +1 -1
- package/dist/motion/transition.d.ts.map +1 -1
- package/dist/motion/types.d.ts +11 -1
- package/dist/motion/types.d.ts.map +1 -1
- package/dist/motion-D9TcHxOF.js +415 -0
- package/dist/motion-D9TcHxOF.js.map +1 -0
- package/dist/motion.es.mjs +25 -361
- package/dist/object-qGpWr6-J.js +38 -0
- package/dist/object-qGpWr6-J.js.map +1 -0
- package/dist/platform/announcer.d.ts +59 -0
- package/dist/platform/announcer.d.ts.map +1 -0
- package/dist/platform/config.d.ts +92 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/cookies.d.ts +45 -0
- package/dist/platform/cookies.d.ts.map +1 -0
- package/dist/platform/index.d.ts +8 -0
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/meta.d.ts +62 -0
- package/dist/platform/meta.d.ts.map +1 -0
- package/dist/platform-Dr9b6fsq.js +362 -0
- package/dist/platform-Dr9b6fsq.js.map +1 -0
- package/dist/platform.es.mjs +11 -248
- package/dist/reactive/async-data.d.ts +114 -0
- package/dist/reactive/async-data.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/signal.d.ts +2 -0
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive-DSkct0dO.js +254 -0
- package/dist/reactive-DSkct0dO.js.map +1 -0
- package/dist/reactive.es.mjs +18 -32
- package/dist/router-CbDhl8rS.js +188 -0
- package/dist/router-CbDhl8rS.js.map +1 -0
- package/dist/router.es.mjs +11 -200
- package/dist/sanitize-Bs2dkMby.js +313 -0
- package/dist/sanitize-Bs2dkMby.js.map +1 -0
- package/dist/security/constants.d.ts.map +1 -1
- package/dist/security/index.d.ts +4 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/sanitize.d.ts +4 -1
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security/trusted-html.d.ts +53 -0
- package/dist/security/trusted-html.d.ts.map +1 -0
- package/dist/security.es.mjs +11 -56
- package/dist/store/define-store.d.ts +1 -1
- package/dist/store/define-store.d.ts.map +1 -1
- package/dist/store/mapping.d.ts +1 -1
- package/dist/store/mapping.d.ts.map +1 -1
- package/dist/store/persisted.d.ts +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/store/types.d.ts +2 -2
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/watch.d.ts +1 -1
- package/dist/store/watch.d.ts.map +1 -1
- package/dist/store-BwDvI45q.js +263 -0
- package/dist/store-BwDvI45q.js.map +1 -0
- package/dist/store.es.mjs +12 -25
- package/dist/storybook/index.d.ts +37 -0
- package/dist/storybook/index.d.ts.map +1 -0
- package/dist/storybook.es.mjs +151 -0
- package/dist/storybook.es.mjs.map +1 -0
- package/dist/untrack-B0rVscTc.js +7 -0
- package/dist/untrack-B0rVscTc.js.map +1 -0
- package/dist/view-C70lA3vf.js +397 -0
- package/dist/view-C70lA3vf.js.map +1 -0
- package/dist/view.es.mjs +11 -430
- package/package.json +141 -132
- package/src/component/component.ts +524 -289
- package/src/component/html.ts +153 -53
- package/src/component/index.ts +50 -40
- package/src/component/library.ts +518 -0
- package/src/component/types.ts +256 -85
- package/src/core/collection.ts +628 -628
- package/src/core/element.ts +774 -774
- package/src/core/index.ts +48 -48
- package/src/core/utils/function.ts +151 -151
- package/src/full.ts +229 -187
- package/src/motion/animate.ts +113 -113
- package/src/motion/flip.ts +176 -176
- package/src/motion/scroll.ts +57 -57
- package/src/motion/spring.ts +150 -150
- package/src/motion/timeline.ts +246 -246
- package/src/motion/transition.ts +97 -51
- package/src/motion/types.ts +11 -1
- package/src/platform/announcer.ts +208 -0
- package/src/platform/config.ts +163 -0
- package/src/platform/cookies.ts +165 -0
- package/src/platform/index.ts +21 -0
- package/src/platform/meta.ts +168 -0
- package/src/platform/storage.ts +215 -215
- package/src/reactive/async-data.ts +486 -0
- package/src/reactive/core.ts +114 -114
- package/src/reactive/effect.ts +54 -54
- package/src/reactive/index.ts +15 -1
- package/src/reactive/internals.ts +122 -122
- package/src/reactive/signal.ts +9 -0
- package/src/security/constants.ts +3 -1
- package/src/security/index.ts +17 -10
- package/src/security/sanitize-core.ts +364 -364
- package/src/security/sanitize.ts +70 -66
- package/src/security/trusted-html.ts +71 -0
- package/src/store/define-store.ts +49 -48
- package/src/store/mapping.ts +74 -73
- package/src/store/persisted.ts +62 -61
- package/src/store/types.ts +92 -94
- package/src/store/watch.ts +53 -52
- package/src/storybook/index.ts +479 -0
- package/src/view/evaluate.ts +290 -290
- package/dist/batch-x7b2eZST.js +0 -13
- package/dist/batch-x7b2eZST.js.map +0 -1
- package/dist/component.es.mjs.map +0 -1
- package/dist/core-BhpuvPhy.js +0 -170
- package/dist/core-BhpuvPhy.js.map +0 -1
- package/dist/core.es.mjs.map +0 -1
- package/dist/full.es.mjs.map +0 -1
- package/dist/index.es.mjs.map +0 -1
- package/dist/motion.es.mjs.map +0 -1
- package/dist/persisted-DHoi3uEs.js +0 -278
- package/dist/persisted-DHoi3uEs.js.map +0 -1
- package/dist/platform.es.mjs.map +0 -1
- package/dist/reactive.es.mjs.map +0 -1
- package/dist/router.es.mjs.map +0 -1
- package/dist/sanitize-Cxvxa-DX.js +0 -283
- package/dist/sanitize-Cxvxa-DX.js.map +0 -1
- package/dist/security.es.mjs.map +0 -1
- package/dist/store.es.mjs.map +0 -1
- package/dist/type-guards-BdKlYYlS.js +0 -32
- package/dist/type-guards-BdKlYYlS.js.map +0 -1
- package/dist/untrack-DNnnqdlR.js +0 -6
- package/dist/untrack-DNnnqdlR.js.map +0 -1
- package/dist/view.es.mjs.map +0 -1
- package/dist/watch-DXXv3iAI.js +0 -58
- package/dist/watch-DXXv3iAI.js.map +0 -1
|
@@ -1,289 +1,524 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web Component factory and registry.
|
|
3
|
-
*
|
|
4
|
-
* @module bquery/component
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { sanitizeHtml } from '../security/sanitize';
|
|
8
|
-
import {
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @
|
|
142
|
-
*/
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Web Component factory and registry.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { sanitizeHtml } from '../security/sanitize';
|
|
8
|
+
import { effect, untrack } from '../reactive/signal';
|
|
9
|
+
import type { CleanupFn } from '../reactive/signal';
|
|
10
|
+
import { coercePropValue } from './props';
|
|
11
|
+
import type {
|
|
12
|
+
AttributeChange,
|
|
13
|
+
ComponentClass,
|
|
14
|
+
ComponentDefinition,
|
|
15
|
+
ComponentSignalLike,
|
|
16
|
+
ComponentSignals,
|
|
17
|
+
ComponentStateShape,
|
|
18
|
+
PropDefinition,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Base extra tags preserved for component shadow DOM renders in addition to the
|
|
23
|
+
* global sanitizer defaults. `slot` must remain allowed here because shadow DOM
|
|
24
|
+
* content projection depends on authored `<slot>` elements in component render
|
|
25
|
+
* output.
|
|
26
|
+
*/
|
|
27
|
+
const COMPONENT_ALLOWED_TAGS = ['slot'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Base extra attributes preserved for component shadow DOM renders in addition
|
|
31
|
+
* to the global sanitizer defaults.
|
|
32
|
+
*/
|
|
33
|
+
const COMPONENT_ALLOWED_ATTRIBUTES = [
|
|
34
|
+
'part',
|
|
35
|
+
// Standard form attributes required by interactive shadow DOM content
|
|
36
|
+
'disabled',
|
|
37
|
+
'checked',
|
|
38
|
+
'placeholder',
|
|
39
|
+
'value',
|
|
40
|
+
'rows',
|
|
41
|
+
'cols',
|
|
42
|
+
'readonly',
|
|
43
|
+
'required',
|
|
44
|
+
'maxlength',
|
|
45
|
+
'minlength',
|
|
46
|
+
'max',
|
|
47
|
+
'min',
|
|
48
|
+
'step',
|
|
49
|
+
'pattern',
|
|
50
|
+
'autocomplete',
|
|
51
|
+
'autofocus',
|
|
52
|
+
'for',
|
|
53
|
+
'multiple',
|
|
54
|
+
'selected',
|
|
55
|
+
'wrap',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a custom element class for a component definition.
|
|
60
|
+
*
|
|
61
|
+
* This is useful when you want to extend or register the class manually
|
|
62
|
+
* (e.g. with different tag names in tests or custom registries).
|
|
63
|
+
*
|
|
64
|
+
* @template TProps - Type of the component's props
|
|
65
|
+
* @param tagName - The custom element tag name (used for diagnostics)
|
|
66
|
+
* @param definition - The component configuration
|
|
67
|
+
*/
|
|
68
|
+
const createComponentClass = <
|
|
69
|
+
TProps extends Record<string, unknown>,
|
|
70
|
+
TState extends Record<string, unknown> | undefined = undefined,
|
|
71
|
+
TSignals extends ComponentSignals = Record<string, never>,
|
|
72
|
+
>(
|
|
73
|
+
tagName: string,
|
|
74
|
+
definition: ComponentDefinition<TProps, TState, TSignals>
|
|
75
|
+
): ComponentClass<TState> => {
|
|
76
|
+
const componentAllowedTags = [...COMPONENT_ALLOWED_TAGS, ...(definition.sanitize?.allowTags ?? [])];
|
|
77
|
+
const componentAllowedAttributes = [
|
|
78
|
+
...COMPONENT_ALLOWED_ATTRIBUTES,
|
|
79
|
+
...(definition.sanitize?.allowAttributes ?? []),
|
|
80
|
+
];
|
|
81
|
+
const signalSources = Object.values(definition.signals ?? {}) as ComponentSignalLike<unknown>[];
|
|
82
|
+
|
|
83
|
+
class BQueryComponent extends HTMLElement {
|
|
84
|
+
/** Internal state object for the component */
|
|
85
|
+
private readonly state: ComponentStateShape<TState> = {
|
|
86
|
+
...(definition.state ?? {}),
|
|
87
|
+
} as ComponentStateShape<TState>;
|
|
88
|
+
/** Typed props object populated from attributes */
|
|
89
|
+
private props = {} as TProps;
|
|
90
|
+
/** Tracks missing required props for validation during connectedCallback */
|
|
91
|
+
private missingRequiredProps = new Set<string>();
|
|
92
|
+
/** Tracks whether the component has completed its initial mount */
|
|
93
|
+
private hasMounted = false;
|
|
94
|
+
/** Cleanup for external signal subscriptions */
|
|
95
|
+
private signalEffectCleanup?: CleanupFn;
|
|
96
|
+
|
|
97
|
+
constructor() {
|
|
98
|
+
super();
|
|
99
|
+
this.attachShadow({ mode: 'open' });
|
|
100
|
+
this.syncProps();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the list of attributes to observe for changes.
|
|
105
|
+
*/
|
|
106
|
+
static get observedAttributes(): string[] {
|
|
107
|
+
return Object.keys(definition.props ?? {});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Called when the element is added to the DOM.
|
|
112
|
+
*/
|
|
113
|
+
connectedCallback(): void {
|
|
114
|
+
try {
|
|
115
|
+
// Defer only the initial mount until all required props are present.
|
|
116
|
+
// Already-mounted components must still reconnect their signal
|
|
117
|
+
// subscriptions so reactive updates can resume after reattachment.
|
|
118
|
+
if (!this.hasMounted && this.missingRequiredProps.size > 0) {
|
|
119
|
+
// Component will mount once all required props are satisfied
|
|
120
|
+
// via attributeChangedCallback
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (this.hasMounted) {
|
|
124
|
+
try {
|
|
125
|
+
definition.connected?.call(this);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.handleError(error as Error);
|
|
128
|
+
}
|
|
129
|
+
this.setupSignalSubscriptions(true);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.mount();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this.handleError(error as Error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Performs the initial mount of the component.
|
|
140
|
+
* Called when the element is connected and all required props are present.
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
private mount(): void {
|
|
144
|
+
if (this.hasMounted) return;
|
|
145
|
+
definition.beforeMount?.call(this);
|
|
146
|
+
definition.connected?.call(this);
|
|
147
|
+
this.render();
|
|
148
|
+
this.setupSignalSubscriptions();
|
|
149
|
+
this.hasMounted = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Called when the element is removed from the DOM.
|
|
154
|
+
*/
|
|
155
|
+
disconnectedCallback(): void {
|
|
156
|
+
try {
|
|
157
|
+
this.signalEffectCleanup?.();
|
|
158
|
+
this.signalEffectCleanup = undefined;
|
|
159
|
+
definition.disconnected?.call(this);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this.handleError(error as Error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Called when an observed attribute changes.
|
|
167
|
+
*/
|
|
168
|
+
attributeChangedCallback(
|
|
169
|
+
name: string,
|
|
170
|
+
oldValue: string | null,
|
|
171
|
+
newValue: string | null
|
|
172
|
+
): void {
|
|
173
|
+
try {
|
|
174
|
+
const previousProps = this.cloneProps();
|
|
175
|
+
this.syncProps();
|
|
176
|
+
|
|
177
|
+
if (this.hasMounted) {
|
|
178
|
+
// Component already mounted - trigger update render
|
|
179
|
+
this.render(true, previousProps, { name, oldValue, newValue });
|
|
180
|
+
} else if (this.isConnected && this.missingRequiredProps.size === 0) {
|
|
181
|
+
// All required props are now satisfied and element is connected
|
|
182
|
+
// Trigger the deferred initial mount
|
|
183
|
+
this.mount();
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.handleError(error as Error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handles errors during component lifecycle.
|
|
192
|
+
* @internal
|
|
193
|
+
*/
|
|
194
|
+
private handleError(error: Error): void {
|
|
195
|
+
if (definition.onError) {
|
|
196
|
+
definition.onError.call(this, error);
|
|
197
|
+
} else {
|
|
198
|
+
console.error(`bQuery component error in <${tagName}>:`, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Updates a state property and triggers a re-render.
|
|
204
|
+
*
|
|
205
|
+
* @param key - The state property key
|
|
206
|
+
* @param value - The new value
|
|
207
|
+
*/
|
|
208
|
+
setState<TKey extends keyof ComponentStateShape<TState>>(
|
|
209
|
+
key: TKey,
|
|
210
|
+
value: ComponentStateShape<TState>[TKey]
|
|
211
|
+
): void {
|
|
212
|
+
this.state[key] = value;
|
|
213
|
+
this.render(true, this.cloneProps(), undefined, false);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Gets a state property value.
|
|
218
|
+
*
|
|
219
|
+
* @param key - The state property key
|
|
220
|
+
* @returns The current value
|
|
221
|
+
*/
|
|
222
|
+
getState<TKey extends keyof ComponentStateShape<TState>>(
|
|
223
|
+
key: TKey
|
|
224
|
+
): ComponentStateShape<TState>[TKey];
|
|
225
|
+
getState<TResult = unknown>(key: string): TResult;
|
|
226
|
+
getState(key: string): unknown {
|
|
227
|
+
return (this.state as Record<string, unknown>)[key];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Subscribes to declared reactive sources and re-renders on change.
|
|
232
|
+
*
|
|
233
|
+
* @param renderOnInitialRun - When true, immediately re-renders after
|
|
234
|
+
* re-subscribing so detached components resync with any signal changes
|
|
235
|
+
* that happened while they were disconnected.
|
|
236
|
+
* @internal
|
|
237
|
+
*/
|
|
238
|
+
private setupSignalSubscriptions(renderOnInitialRun = false): void {
|
|
239
|
+
if (this.signalEffectCleanup || signalSources.length === 0) return;
|
|
240
|
+
|
|
241
|
+
let isInitialRun = true;
|
|
242
|
+
this.signalEffectCleanup = effect(() => {
|
|
243
|
+
try {
|
|
244
|
+
for (const source of signalSources) {
|
|
245
|
+
// Intentionally read each source to register this effect as a subscriber.
|
|
246
|
+
void source.value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (isInitialRun) {
|
|
250
|
+
isInitialRun = false;
|
|
251
|
+
if (renderOnInitialRun && this.hasMounted && this.isConnected) {
|
|
252
|
+
// Signal-driven reconnect renders do not change props, so the
|
|
253
|
+
// previous-props snapshot is the current prop set at reconnect time.
|
|
254
|
+
const previousProps = this.cloneProps();
|
|
255
|
+
untrack(() => {
|
|
256
|
+
this.render(true, previousProps, undefined, false);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!this.hasMounted || !this.isConnected) return;
|
|
263
|
+
|
|
264
|
+
// Signal updates leave props unchanged, so cloning the current props
|
|
265
|
+
// provides the previous-props snapshot expected by beforeUpdate().
|
|
266
|
+
const previousProps = this.cloneProps();
|
|
267
|
+
untrack(() => {
|
|
268
|
+
this.render(true, previousProps, undefined, false);
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
this.handleError(error as Error);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Synchronizes props from attributes.
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
private syncProps(): void {
|
|
281
|
+
const props = definition.props ?? {};
|
|
282
|
+
for (const [key, config] of Object.entries(props) as [string, PropDefinition][]) {
|
|
283
|
+
const attrValue = this.getAttribute(key);
|
|
284
|
+
let value: unknown;
|
|
285
|
+
|
|
286
|
+
if (attrValue == null) {
|
|
287
|
+
if (config.required && config.default === undefined) {
|
|
288
|
+
// Mark as missing instead of throwing - validate during connectedCallback
|
|
289
|
+
this.missingRequiredProps.add(key);
|
|
290
|
+
value = undefined;
|
|
291
|
+
} else {
|
|
292
|
+
value = config.default ?? undefined;
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
// Attribute is present, remove from missing set if it was there
|
|
296
|
+
if (this.missingRequiredProps.has(key)) {
|
|
297
|
+
this.missingRequiredProps.delete(key);
|
|
298
|
+
}
|
|
299
|
+
value = coercePropValue(attrValue, config);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (config.validator && value !== undefined) {
|
|
303
|
+
const isValid = config.validator(value);
|
|
304
|
+
if (!isValid) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`bQuery component: validation failed for prop "${key}" with value ${JSON.stringify(value)}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
(this.props as Record<string, unknown>)[key] = value;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Creates a shallow snapshot of the current props for lifecycle diffing.
|
|
317
|
+
* A shallow copy is sufficient because component props are re-derived from
|
|
318
|
+
* reflected attributes on each update, so nested object mutation is not
|
|
319
|
+
* tracked as part of this lifecycle diff.
|
|
320
|
+
* @internal
|
|
321
|
+
*/
|
|
322
|
+
private cloneProps(): TProps {
|
|
323
|
+
return { ...(this.props as Record<string, unknown>) } as TProps;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Renders the component to its shadow root.
|
|
328
|
+
* @internal
|
|
329
|
+
*/
|
|
330
|
+
private render(): void;
|
|
331
|
+
private render(triggerUpdated: true, oldProps: TProps, change?: AttributeChange): void;
|
|
332
|
+
private render(
|
|
333
|
+
triggerUpdated: true,
|
|
334
|
+
oldProps: TProps,
|
|
335
|
+
change: AttributeChange | undefined,
|
|
336
|
+
runBeforeUpdate: boolean
|
|
337
|
+
): void;
|
|
338
|
+
private render(
|
|
339
|
+
triggerUpdated = false,
|
|
340
|
+
oldProps?: TProps,
|
|
341
|
+
change?: AttributeChange,
|
|
342
|
+
runBeforeUpdate = true
|
|
343
|
+
): void {
|
|
344
|
+
try {
|
|
345
|
+
if (triggerUpdated && runBeforeUpdate && definition.beforeUpdate) {
|
|
346
|
+
if (!oldProps) {
|
|
347
|
+
throw new Error('bQuery component: previous props are required for update renders');
|
|
348
|
+
}
|
|
349
|
+
const shouldUpdate = definition.beforeUpdate.call(this, this.props, oldProps);
|
|
350
|
+
if (shouldUpdate === false) return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const emit = (event: string, detail?: unknown): void => {
|
|
354
|
+
this.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, composed: true }));
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (!this.shadowRoot) return;
|
|
358
|
+
|
|
359
|
+
const markup = definition.render({
|
|
360
|
+
props: this.props,
|
|
361
|
+
state: this.state,
|
|
362
|
+
signals: (definition.signals ?? {}) as TSignals,
|
|
363
|
+
emit,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Component render output is authored by the component definition itself,
|
|
367
|
+
// so we can explicitly preserve shadow-DOM-specific markup such as <slot>,
|
|
368
|
+
// the stylistic `part` attribute, and standard form/input attributes without
|
|
369
|
+
// relaxing the global DOM sanitization rules.
|
|
370
|
+
const sanitizedMarkup = sanitizeHtml(markup, {
|
|
371
|
+
allowTags: componentAllowedTags,
|
|
372
|
+
allowAttributes: componentAllowedAttributes,
|
|
373
|
+
});
|
|
374
|
+
let existingStyleElement: HTMLStyleElement | null = null;
|
|
375
|
+
if (definition.styles) {
|
|
376
|
+
existingStyleElement = this.shadowRoot.querySelector<HTMLStyleElement>(
|
|
377
|
+
'style[data-bquery-component-style]'
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.shadowRoot.innerHTML = sanitizedMarkup;
|
|
382
|
+
|
|
383
|
+
if (definition.styles) {
|
|
384
|
+
const styleElement = existingStyleElement ?? document.createElement('style');
|
|
385
|
+
if (!existingStyleElement) {
|
|
386
|
+
styleElement.setAttribute('data-bquery-component-style', '');
|
|
387
|
+
}
|
|
388
|
+
styleElement.textContent = definition.styles;
|
|
389
|
+
this.shadowRoot.prepend(styleElement);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (triggerUpdated) {
|
|
393
|
+
definition.updated?.call(this, change);
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
this.handleError(error as Error);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return BQueryComponent as ComponentClass<TState>;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Creates a custom element class for a component definition.
|
|
406
|
+
*
|
|
407
|
+
* This is useful when you want to extend or register the class manually
|
|
408
|
+
* (e.g. with different tag names in tests or custom registries).
|
|
409
|
+
*
|
|
410
|
+
* @template TProps - Type of the component's props
|
|
411
|
+
* @template TState - Type of the component's internal state. When provided,
|
|
412
|
+
* `definition.state` is required, `render({ state })` is strongly typed, and
|
|
413
|
+
* returned instances expose typed `getState()` / `setState()` helpers.
|
|
414
|
+
* @param tagName - The custom element tag name (used for diagnostics)
|
|
415
|
+
* @param definition - The component configuration
|
|
416
|
+
*/
|
|
417
|
+
export function defineComponent<
|
|
418
|
+
TProps extends Record<string, unknown>,
|
|
419
|
+
TSignals extends ComponentSignals = Record<string, never>,
|
|
420
|
+
>(
|
|
421
|
+
tagName: string,
|
|
422
|
+
definition: ComponentDefinition<TProps, undefined, TSignals>
|
|
423
|
+
): ComponentClass<undefined>;
|
|
424
|
+
export function defineComponent<
|
|
425
|
+
TProps extends Record<string, unknown>,
|
|
426
|
+
TState extends Record<string, unknown>,
|
|
427
|
+
TSignals extends ComponentSignals = Record<string, never>,
|
|
428
|
+
>(
|
|
429
|
+
tagName: string,
|
|
430
|
+
definition: ComponentDefinition<TProps, TState, TSignals>
|
|
431
|
+
): ComponentClass<TState>;
|
|
432
|
+
export function defineComponent<
|
|
433
|
+
TProps extends Record<string, unknown>,
|
|
434
|
+
TState extends Record<string, unknown> | undefined = undefined,
|
|
435
|
+
TSignals extends ComponentSignals = Record<string, never>,
|
|
436
|
+
>(
|
|
437
|
+
tagName: string,
|
|
438
|
+
definition: ComponentDefinition<TProps, TState, TSignals>
|
|
439
|
+
): ComponentClass<TState> {
|
|
440
|
+
return createComponentClass(tagName, definition);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Defines and registers a custom Web Component.
|
|
445
|
+
*
|
|
446
|
+
* This function creates a new custom element with the given tag name
|
|
447
|
+
* and configuration. The component uses Shadow DOM for encapsulation
|
|
448
|
+
* and automatically re-renders when observed attributes change.
|
|
449
|
+
*
|
|
450
|
+
* @template TProps - Type of the component's props
|
|
451
|
+
* @template TState - Type of the component's internal state. When provided,
|
|
452
|
+
* `definition.state` is required and lifecycle hooks receive typed state
|
|
453
|
+
* helpers via `this.getState()` / `this.setState()`.
|
|
454
|
+
* @param tagName - The custom element tag name (must contain a hyphen)
|
|
455
|
+
* @param definition - The component configuration
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```ts
|
|
459
|
+
* component<{ start: number }, { count: number }>('counter-button', {
|
|
460
|
+
* props: {
|
|
461
|
+
* start: { type: Number, default: 0 },
|
|
462
|
+
* },
|
|
463
|
+
* state: { count: 0 },
|
|
464
|
+
* styles: `
|
|
465
|
+
* button { padding: 0.5rem 1rem; }
|
|
466
|
+
* `,
|
|
467
|
+
* connected() {
|
|
468
|
+
* // Use event delegation on shadow root so handler survives re-renders
|
|
469
|
+
* const handleClick = (event: Event) => {
|
|
470
|
+
* const target = event.target as HTMLElement | null;
|
|
471
|
+
* if (target?.matches('button')) {
|
|
472
|
+
* this.setState('count', this.getState('count') + 1);
|
|
473
|
+
* }
|
|
474
|
+
* };
|
|
475
|
+
* this.shadowRoot?.addEventListener('click', handleClick);
|
|
476
|
+
* // Store handler for cleanup
|
|
477
|
+
* (this as any)._handleClick = handleClick;
|
|
478
|
+
* },
|
|
479
|
+
* disconnected() {
|
|
480
|
+
* // Clean up event listener to prevent memory leaks
|
|
481
|
+
* const handleClick = (this as any)._handleClick;
|
|
482
|
+
* if (handleClick) {
|
|
483
|
+
* this.shadowRoot?.removeEventListener('click', handleClick);
|
|
484
|
+
* }
|
|
485
|
+
* },
|
|
486
|
+
* render({ props, state }) {
|
|
487
|
+
* return html`
|
|
488
|
+
* <button>
|
|
489
|
+
* Count: ${state.count}
|
|
490
|
+
* </button>
|
|
491
|
+
* `;
|
|
492
|
+
* },
|
|
493
|
+
* });
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
export function component<
|
|
497
|
+
TProps extends Record<string, unknown>,
|
|
498
|
+
TSignals extends ComponentSignals = Record<string, never>,
|
|
499
|
+
>(
|
|
500
|
+
tagName: string,
|
|
501
|
+
definition: ComponentDefinition<TProps, undefined, TSignals>
|
|
502
|
+
): void;
|
|
503
|
+
export function component<
|
|
504
|
+
TProps extends Record<string, unknown>,
|
|
505
|
+
TState extends Record<string, unknown>,
|
|
506
|
+
TSignals extends ComponentSignals = Record<string, never>,
|
|
507
|
+
>(
|
|
508
|
+
tagName: string,
|
|
509
|
+
definition: ComponentDefinition<TProps, TState, TSignals>
|
|
510
|
+
): void;
|
|
511
|
+
export function component<
|
|
512
|
+
TProps extends Record<string, unknown>,
|
|
513
|
+
TState extends Record<string, unknown> | undefined = undefined,
|
|
514
|
+
TSignals extends ComponentSignals = Record<string, never>,
|
|
515
|
+
>(
|
|
516
|
+
tagName: string,
|
|
517
|
+
definition: ComponentDefinition<TProps, TState, TSignals>
|
|
518
|
+
): void {
|
|
519
|
+
const elementClass = createComponentClass(tagName, definition);
|
|
520
|
+
|
|
521
|
+
if (!customElements.get(tagName)) {
|
|
522
|
+
customElements.define(tagName, elementClass);
|
|
523
|
+
}
|
|
524
|
+
}
|