@bquery/bquery 1.0.1 → 1.1.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 +79 -25
- package/dist/component/index.d.ts +8 -0
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component.es.mjs +80 -53
- package/dist/component.es.mjs.map +1 -1
- package/dist/core/collection.d.ts +46 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +124 -22
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/utils.d.ts +13 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core.es.mjs +298 -55
- package/dist/core.es.mjs.map +1 -1
- package/dist/full.d.ts +2 -2
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +38 -33
- package/dist/full.iife.js +1 -1
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +1 -1
- package/dist/full.umd.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.es.mjs +38 -33
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/signal.d.ts +107 -0
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive.es.mjs +92 -55
- package/dist/reactive.es.mjs.map +1 -1
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security.es.mjs +136 -66
- package/dist/security.es.mjs.map +1 -1
- package/package.json +120 -120
- package/src/component/index.ts +414 -360
- package/src/core/collection.ts +454 -339
- package/src/core/element.ts +740 -493
- package/src/core/utils.ts +444 -425
- package/src/full.ts +106 -101
- package/src/index.ts +27 -27
- package/src/reactive/index.ts +22 -9
- package/src/reactive/signal.ts +506 -347
- package/src/security/sanitize.ts +553 -446
package/src/component/index.ts
CHANGED
|
@@ -1,360 +1,414 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal Web Component helper for building custom elements.
|
|
3
|
-
*
|
|
4
|
-
* This module provides a declarative API for defining Web Components
|
|
5
|
-
* without complex build steps. Features include:
|
|
6
|
-
* - Type-safe props with automatic attribute coercion
|
|
7
|
-
* - Reactive state management
|
|
8
|
-
* - Shadow DOM encapsulation with scoped styles
|
|
9
|
-
* - Lifecycle hooks (connected, disconnected)
|
|
10
|
-
* - Event emission helpers
|
|
11
|
-
*
|
|
12
|
-
* @module bquery/component
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* import { component, html } from 'bquery/component';
|
|
17
|
-
*
|
|
18
|
-
* component('user-card', {
|
|
19
|
-
* props: {
|
|
20
|
-
* username: { type: String, required: true },
|
|
21
|
-
* avatar: { type: String, default: '/default-avatar.png' },
|
|
22
|
-
* },
|
|
23
|
-
* styles: `
|
|
24
|
-
* .card { padding: 1rem; border: 1px solid #ccc; }
|
|
25
|
-
* `,
|
|
26
|
-
* render({ props }) {
|
|
27
|
-
* return html`
|
|
28
|
-
* <div class="card">
|
|
29
|
-
* <img src="${props.avatar}" alt="${props.username}" />
|
|
30
|
-
* <h3>${props.username}</h3>
|
|
31
|
-
* </div>
|
|
32
|
-
* `;
|
|
33
|
-
* },
|
|
34
|
-
* });
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Defines a single prop's type and configuration.
|
|
40
|
-
*
|
|
41
|
-
* @template T - The TypeScript type of the prop value
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* ```ts
|
|
45
|
-
* const myProp: PropDefinition<number> = {
|
|
46
|
-
* type: Number,
|
|
47
|
-
* required: false,
|
|
48
|
-
* default: 0,
|
|
49
|
-
* };
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export type PropDefinition<T = unknown> = {
|
|
53
|
-
/** Constructor or converter function for the prop type */
|
|
54
|
-
type:
|
|
55
|
-
| StringConstructor
|
|
56
|
-
| NumberConstructor
|
|
57
|
-
| BooleanConstructor
|
|
58
|
-
| ObjectConstructor
|
|
59
|
-
| ArrayConstructor
|
|
60
|
-
| { new (value: unknown): T }
|
|
61
|
-
| ((value: unknown) => T);
|
|
62
|
-
/** Whether the prop must be provided */
|
|
63
|
-
required?: boolean;
|
|
64
|
-
/** Default value when prop is not provided */
|
|
65
|
-
default?: T;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
/** Lifecycle hook called
|
|
84
|
-
|
|
85
|
-
/** Lifecycle hook called
|
|
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
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return
|
|
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
|
-
* Called when the element is
|
|
272
|
-
*/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
*
|
|
308
|
-
* @internal
|
|
309
|
-
*/
|
|
310
|
-
private
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
*
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Web Component helper for building custom elements.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a declarative API for defining Web Components
|
|
5
|
+
* without complex build steps. Features include:
|
|
6
|
+
* - Type-safe props with automatic attribute coercion
|
|
7
|
+
* - Reactive state management
|
|
8
|
+
* - Shadow DOM encapsulation with scoped styles
|
|
9
|
+
* - Lifecycle hooks (connected, disconnected)
|
|
10
|
+
* - Event emission helpers
|
|
11
|
+
*
|
|
12
|
+
* @module bquery/component
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { component, html } from 'bquery/component';
|
|
17
|
+
*
|
|
18
|
+
* component('user-card', {
|
|
19
|
+
* props: {
|
|
20
|
+
* username: { type: String, required: true },
|
|
21
|
+
* avatar: { type: String, default: '/default-avatar.png' },
|
|
22
|
+
* },
|
|
23
|
+
* styles: `
|
|
24
|
+
* .card { padding: 1rem; border: 1px solid #ccc; }
|
|
25
|
+
* `,
|
|
26
|
+
* render({ props }) {
|
|
27
|
+
* return html`
|
|
28
|
+
* <div class="card">
|
|
29
|
+
* <img src="${props.avatar}" alt="${props.username}" />
|
|
30
|
+
* <h3>${props.username}</h3>
|
|
31
|
+
* </div>
|
|
32
|
+
* `;
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Defines a single prop's type and configuration.
|
|
40
|
+
*
|
|
41
|
+
* @template T - The TypeScript type of the prop value
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const myProp: PropDefinition<number> = {
|
|
46
|
+
* type: Number,
|
|
47
|
+
* required: false,
|
|
48
|
+
* default: 0,
|
|
49
|
+
* };
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export type PropDefinition<T = unknown> = {
|
|
53
|
+
/** Constructor or converter function for the prop type */
|
|
54
|
+
type:
|
|
55
|
+
| StringConstructor
|
|
56
|
+
| NumberConstructor
|
|
57
|
+
| BooleanConstructor
|
|
58
|
+
| ObjectConstructor
|
|
59
|
+
| ArrayConstructor
|
|
60
|
+
| { new (value: unknown): T }
|
|
61
|
+
| ((value: unknown) => T);
|
|
62
|
+
/** Whether the prop must be provided */
|
|
63
|
+
required?: boolean;
|
|
64
|
+
/** Default value when prop is not provided */
|
|
65
|
+
default?: T;
|
|
66
|
+
/** Optional validator function to validate prop values */
|
|
67
|
+
validator?: (value: T) => boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Complete component definition including props, state, styles, and lifecycle.
|
|
72
|
+
*
|
|
73
|
+
* @template TProps - Type of the component's props
|
|
74
|
+
*/
|
|
75
|
+
export type ComponentDefinition<TProps extends Record<string, unknown> = Record<string, unknown>> =
|
|
76
|
+
{
|
|
77
|
+
/** Prop definitions with types and defaults */
|
|
78
|
+
props?: Record<keyof TProps, PropDefinition>;
|
|
79
|
+
/** Initial internal state */
|
|
80
|
+
state?: Record<string, unknown>;
|
|
81
|
+
/** CSS styles scoped to the component's shadow DOM */
|
|
82
|
+
styles?: string;
|
|
83
|
+
/** Lifecycle hook called before the component mounts (before first render) */
|
|
84
|
+
beforeMount?: () => void;
|
|
85
|
+
/** Lifecycle hook called when component is added to DOM */
|
|
86
|
+
connected?: () => void;
|
|
87
|
+
/** Lifecycle hook called when component is removed from DOM */
|
|
88
|
+
disconnected?: () => void;
|
|
89
|
+
/** Lifecycle hook called before an update render; return false to prevent */
|
|
90
|
+
beforeUpdate?: (props: TProps) => boolean | void;
|
|
91
|
+
/** Lifecycle hook called after reactive updates trigger a render */
|
|
92
|
+
updated?: () => void;
|
|
93
|
+
/** Error handler for errors during rendering or lifecycle */
|
|
94
|
+
onError?: (error: Error) => void;
|
|
95
|
+
/** Render function returning HTML string */
|
|
96
|
+
render: (context: {
|
|
97
|
+
props: TProps;
|
|
98
|
+
state: Record<string, unknown>;
|
|
99
|
+
emit: (event: string, detail?: unknown) => void;
|
|
100
|
+
}) => string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Coerces a string attribute value into a typed prop value.
|
|
105
|
+
* Supports String, Number, Boolean, Object, Array, and custom converters.
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
108
|
+
* @template T - The target type
|
|
109
|
+
* @param rawValue - The raw string value from the attribute
|
|
110
|
+
* @param config - The prop definition with type information
|
|
111
|
+
* @returns The coerced value of type T
|
|
112
|
+
*/
|
|
113
|
+
const coercePropValue = <T>(rawValue: string, config: PropDefinition<T>): T => {
|
|
114
|
+
const { type } = config;
|
|
115
|
+
|
|
116
|
+
if (type === String) return rawValue as T;
|
|
117
|
+
|
|
118
|
+
if (type === Number) {
|
|
119
|
+
const parsed = Number(rawValue);
|
|
120
|
+
return (Number.isNaN(parsed) ? rawValue : parsed) as T;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (type === Boolean) {
|
|
124
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
125
|
+
if (normalized === '' || normalized === 'true' || normalized === '1') {
|
|
126
|
+
return true as T;
|
|
127
|
+
}
|
|
128
|
+
if (normalized === 'false' || normalized === '0') {
|
|
129
|
+
return false as T;
|
|
130
|
+
}
|
|
131
|
+
return Boolean(rawValue) as T;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (type === Object || type === Array) {
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(rawValue) as T;
|
|
137
|
+
} catch {
|
|
138
|
+
return rawValue as T;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof type === 'function') {
|
|
143
|
+
const callable = type as (value: unknown) => T;
|
|
144
|
+
const constructable = type as new (value: unknown) => T;
|
|
145
|
+
try {
|
|
146
|
+
return callable(rawValue);
|
|
147
|
+
} catch {
|
|
148
|
+
return new constructable(rawValue);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return rawValue as T;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Tagged template literal for creating HTML strings.
|
|
157
|
+
*
|
|
158
|
+
* This function handles interpolation of values into HTML templates,
|
|
159
|
+
* converting null/undefined to empty strings.
|
|
160
|
+
*
|
|
161
|
+
* @param strings - Template literal string parts
|
|
162
|
+
* @param values - Interpolated values
|
|
163
|
+
* @returns Combined HTML string
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* const name = 'World';
|
|
168
|
+
* const greeting = html`<h1>Hello, ${name}!</h1>`;
|
|
169
|
+
* // Result: '<h1>Hello, World!</h1>'
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export const html = (strings: TemplateStringsArray, ...values: unknown[]): string => {
|
|
173
|
+
return strings.reduce((acc, part, index) => `${acc}${part}${values[index] ?? ''}`, '');
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Escapes HTML entities in interpolated values for XSS prevention.
|
|
178
|
+
* Use this when you need to safely embed user content in templates.
|
|
179
|
+
*
|
|
180
|
+
* @param strings - Template literal string parts
|
|
181
|
+
* @param values - Interpolated values to escape
|
|
182
|
+
* @returns Combined HTML string with escaped values
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* const userInput = '<script>alert("xss")</script>';
|
|
187
|
+
* const safe = safeHtml`<div>${userInput}</div>`;
|
|
188
|
+
* // Result: '<div><script>alert("xss")</script></div>'
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export const safeHtml = (strings: TemplateStringsArray, ...values: unknown[]): string => {
|
|
192
|
+
const escapeMap: Record<string, string> = {
|
|
193
|
+
'&': '&',
|
|
194
|
+
'<': '<',
|
|
195
|
+
'>': '>',
|
|
196
|
+
'"': '"',
|
|
197
|
+
"'": ''',
|
|
198
|
+
'`': '`',
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const escape = (value: unknown): string => {
|
|
202
|
+
const str = String(value ?? '');
|
|
203
|
+
return str.replace(/[&<>"'`]/g, (char) => escapeMap[char]);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return strings.reduce((acc, part, index) => `${acc}${part}${escape(values[index])}`, '');
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Defines and registers a custom Web Component.
|
|
211
|
+
*
|
|
212
|
+
* This function creates a new custom element with the given tag name
|
|
213
|
+
* and configuration. The component uses Shadow DOM for encapsulation
|
|
214
|
+
* and automatically re-renders when observed attributes change.
|
|
215
|
+
*
|
|
216
|
+
* @template TProps - Type of the component's props
|
|
217
|
+
* @param tagName - The custom element tag name (must contain a hyphen)
|
|
218
|
+
* @param definition - The component configuration
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* component('counter-button', {
|
|
223
|
+
* props: {
|
|
224
|
+
* start: { type: Number, default: 0 },
|
|
225
|
+
* },
|
|
226
|
+
* state: { count: 0 },
|
|
227
|
+
* styles: `
|
|
228
|
+
* button { padding: 0.5rem 1rem; }
|
|
229
|
+
* `,
|
|
230
|
+
* connected() {
|
|
231
|
+
* console.log('Counter mounted');
|
|
232
|
+
* },
|
|
233
|
+
* render({ props, state, emit }) {
|
|
234
|
+
* return html`
|
|
235
|
+
* <button onclick="this.getRootNode().host.increment()">
|
|
236
|
+
* Count: ${state.count}
|
|
237
|
+
* </button>
|
|
238
|
+
* `;
|
|
239
|
+
* },
|
|
240
|
+
* });
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
export const component = <TProps extends Record<string, unknown>>(
|
|
244
|
+
tagName: string,
|
|
245
|
+
definition: ComponentDefinition<TProps>
|
|
246
|
+
): void => {
|
|
247
|
+
/**
|
|
248
|
+
* Internal Web Component class created for each component definition.
|
|
249
|
+
* @internal
|
|
250
|
+
*/
|
|
251
|
+
class BQueryComponent extends HTMLElement {
|
|
252
|
+
/** Internal state object for the component */
|
|
253
|
+
private readonly state = { ...(definition.state ?? {}) };
|
|
254
|
+
/** Typed props object populated from attributes */
|
|
255
|
+
private props = {} as TProps;
|
|
256
|
+
|
|
257
|
+
constructor() {
|
|
258
|
+
super();
|
|
259
|
+
this.attachShadow({ mode: 'open' });
|
|
260
|
+
this.syncProps();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Returns the list of attributes to observe for changes.
|
|
265
|
+
*/
|
|
266
|
+
static get observedAttributes(): string[] {
|
|
267
|
+
return Object.keys(definition.props ?? {});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Called when the element is added to the DOM.
|
|
272
|
+
*/
|
|
273
|
+
connectedCallback(): void {
|
|
274
|
+
try {
|
|
275
|
+
definition.beforeMount?.call(this);
|
|
276
|
+
definition.connected?.call(this);
|
|
277
|
+
this.render();
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.handleError(error as Error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Called when the element is removed from the DOM.
|
|
285
|
+
*/
|
|
286
|
+
disconnectedCallback(): void {
|
|
287
|
+
try {
|
|
288
|
+
definition.disconnected?.call(this);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
this.handleError(error as Error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Called when an observed attribute changes.
|
|
296
|
+
*/
|
|
297
|
+
attributeChangedCallback(): void {
|
|
298
|
+
try {
|
|
299
|
+
this.syncProps();
|
|
300
|
+
this.render(true);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
this.handleError(error as Error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Handles errors during component lifecycle.
|
|
308
|
+
* @internal
|
|
309
|
+
*/
|
|
310
|
+
private handleError(error: Error): void {
|
|
311
|
+
if (definition.onError) {
|
|
312
|
+
definition.onError.call(this, error);
|
|
313
|
+
} else {
|
|
314
|
+
console.error(`bQuery component error in <${tagName}>:`, error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Updates a state property and triggers a re-render.
|
|
320
|
+
*
|
|
321
|
+
* @param key - The state property key
|
|
322
|
+
* @param value - The new value
|
|
323
|
+
*/
|
|
324
|
+
setState(key: string, value: unknown): void {
|
|
325
|
+
this.state[key] = value;
|
|
326
|
+
this.render(true);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Gets a state property value.
|
|
331
|
+
*
|
|
332
|
+
* @param key - The state property key
|
|
333
|
+
* @returns The current value
|
|
334
|
+
*/
|
|
335
|
+
getState<T = unknown>(key: string): T {
|
|
336
|
+
return this.state[key] as T;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Synchronizes props from attributes.
|
|
341
|
+
* @internal
|
|
342
|
+
*/
|
|
343
|
+
private syncProps(): void {
|
|
344
|
+
const props = definition.props ?? {};
|
|
345
|
+
for (const [key, config] of Object.entries(props) as [string, PropDefinition][]) {
|
|
346
|
+
const attrValue = this.getAttribute(key);
|
|
347
|
+
let value: unknown;
|
|
348
|
+
|
|
349
|
+
if (attrValue == null) {
|
|
350
|
+
if (config.required && config.default === undefined) {
|
|
351
|
+
throw new Error(`bQuery component: missing required prop "${key}"`);
|
|
352
|
+
}
|
|
353
|
+
value = config.default ?? undefined;
|
|
354
|
+
} else {
|
|
355
|
+
value = coercePropValue(attrValue, config);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Validate the prop value if a validator is provided
|
|
359
|
+
if (config.validator && value !== undefined) {
|
|
360
|
+
const isValid = config.validator(value);
|
|
361
|
+
if (!isValid) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`bQuery component: validation failed for prop "${key}" with value ${JSON.stringify(value)}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
(this.props as Record<string, unknown>)[key] = value;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Renders the component to its shadow root.
|
|
374
|
+
* @internal
|
|
375
|
+
*/
|
|
376
|
+
private render(triggerUpdated = false): void {
|
|
377
|
+
try {
|
|
378
|
+
// Check beforeUpdate hook if this is an update
|
|
379
|
+
if (triggerUpdated && definition.beforeUpdate) {
|
|
380
|
+
const shouldUpdate = definition.beforeUpdate.call(this, this.props);
|
|
381
|
+
if (shouldUpdate === false) return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Emits a custom event from the component.
|
|
386
|
+
*/
|
|
387
|
+
const emit = (event: string, detail?: unknown): void => {
|
|
388
|
+
this.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, composed: true }));
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (!this.shadowRoot) return;
|
|
392
|
+
|
|
393
|
+
const markup = definition.render({
|
|
394
|
+
props: this.props,
|
|
395
|
+
state: this.state,
|
|
396
|
+
emit,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const styles = definition.styles ? `<style>${definition.styles}</style>` : '';
|
|
400
|
+
this.shadowRoot.innerHTML = `${styles}${markup}`;
|
|
401
|
+
|
|
402
|
+
if (triggerUpdated) {
|
|
403
|
+
definition.updated?.call(this);
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.handleError(error as Error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!customElements.get(tagName)) {
|
|
412
|
+
customElements.define(tagName, BQueryComponent);
|
|
413
|
+
}
|
|
414
|
+
};
|