p-elements-core 1.2.32-rc2 → 1.2.32-rc4
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/.editorconfig +17 -17
- package/.gitlab-ci.yml +18 -18
- package/CHANGELOG.md +201 -0
- package/demo/sample.js +1 -1
- package/demo/screen.css +16 -16
- package/dist/p-elements-core-modern.js +1 -1
- package/dist/p-elements-core.js +1 -1
- package/docs/package-lock.json +6897 -6897
- package/docs/package.json +27 -27
- package/docs/src/404.md +8 -8
- package/docs/src/_data/demos/hello-world/hello-world.tsx +35 -35
- package/docs/src/_data/demos/hello-world/index.html +10 -10
- package/docs/src/_data/demos/hello-world/project.json +7 -7
- package/docs/src/_data/demos/timer/demo-timer.tsx +120 -120
- package/docs/src/_data/demos/timer/icons.tsx +62 -62
- package/docs/src/_data/demos/timer/index.html +12 -12
- package/docs/src/_data/demos/timer/project.json +8 -8
- package/docs/src/_data/global.js +13 -13
- package/docs/src/_data/helpers.js +19 -19
- package/docs/src/_includes/layouts/base.njk +30 -30
- package/docs/src/_includes/layouts/playground.njk +40 -40
- package/docs/src/_includes/partials/app-header.njk +8 -8
- package/docs/src/_includes/partials/head.njk +14 -14
- package/docs/src/_includes/partials/nav.njk +19 -19
- package/docs/src/_includes/partials/top-nav.njk +51 -51
- package/docs/src/documentation/custom-element.md +221 -221
- package/docs/src/documentation/decorators/bind.md +71 -71
- package/docs/src/documentation/decorators/custom-element-config.md +63 -63
- package/docs/src/documentation/decorators/property.md +83 -83
- package/docs/src/documentation/decorators/query.md +66 -66
- package/docs/src/documentation/decorators/render-property-on-set.md +60 -60
- package/docs/src/documentation/decorators.md +9 -9
- package/docs/src/documentation/reactive-properties.md +53 -53
- package/docs/src/index.d.ts +25 -25
- package/docs/src/index.md +3 -3
- package/docs/src/scripts/components/app-mode-switch/app-mode-switch.css +78 -78
- package/docs/src/scripts/components/app-mode-switch/app-mode-switch.tsx +166 -166
- package/docs/src/scripts/components/app-playground/app-playground.tsx +189 -189
- package/docs/tsconfig.json +22 -22
- package/package.json +9 -2
- package/readme.md +206 -206
- package/src/custom-element-controller.test.ts +226 -0
- package/src/custom-element-controller.ts +31 -31
- package/src/custom-element.test.ts +906 -0
- package/src/custom-element.ts +17 -1
- package/src/custom-style-element.ts +4 -1
- package/src/decorators/bind.test.ts +163 -0
- package/src/decorators/bind.ts +46 -46
- package/src/decorators/custom-element-config.ts +17 -17
- package/src/decorators/property.test.ts +279 -0
- package/src/decorators/property.ts +789 -684
- package/src/decorators/query.test.ts +146 -0
- package/src/decorators/query.ts +12 -12
- package/src/decorators/render-property-on-set.ts +3 -3
- package/src/helpers/css.test.ts +150 -0
- package/src/helpers/css.ts +71 -71
- package/src/maquette/cache.test.ts +150 -0
- package/src/maquette/cache.ts +35 -35
- package/src/maquette/dom.test.ts +263 -0
- package/src/maquette/dom.ts +115 -115
- package/src/maquette/h.test.ts +165 -0
- package/src/maquette/h.ts +100 -100
- package/src/maquette/index.ts +12 -12
- package/src/maquette/interfaces.ts +536 -536
- package/src/maquette/jsx.ts +61 -61
- package/src/maquette/mapping.test.ts +294 -0
- package/src/maquette/mapping.ts +56 -56
- package/src/maquette/maquette.test.ts +493 -0
- package/src/maquette/projection.test.ts +366 -0
- package/src/maquette/projection.ts +666 -666
- package/src/maquette/projector.test.ts +351 -0
- package/src/maquette/projector.ts +200 -200
- package/src/sample/mixin/highlight.tsx +33 -33
- package/src/test-setup.ts +85 -0
- package/src/test-utils.ts +223 -0
- package/tsconfig.json +1 -0
- package/vitest.config.ts +41 -0
- package/webpack.config.js +1 -1
|
@@ -1,684 +1,789 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supported property type constructors for reactive properties.
|
|
3
|
-
* These constructors are used to convert attribute values to the correct JavaScript type.
|
|
4
|
-
*
|
|
5
|
-
* @typedef {StringConstructor | NumberConstructor | BooleanConstructor | ObjectConstructor | ArrayConstructor} PropertyType
|
|
6
|
-
*/
|
|
7
|
-
type PropertyType =
|
|
8
|
-
| StringConstructor
|
|
9
|
-
| NumberConstructor
|
|
10
|
-
| BooleanConstructor
|
|
11
|
-
| ObjectConstructor
|
|
12
|
-
| ArrayConstructor;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Legacy string-based property types (deprecated).
|
|
16
|
-
* Maintained for backward compatibility. Use PropertyType constructors instead.
|
|
17
|
-
*
|
|
18
|
-
* @typedef {('string' | 'number' | 'boolean' | 'object' | 'array')} oldPropertyType
|
|
19
|
-
* @deprecated Use PropertyType constructors (String, Number, Boolean, Object, Array) instead
|
|
20
|
-
*/
|
|
21
|
-
type oldPropertyType = "string" | "number" | "boolean" | "object" | "array";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Configuration options for the @property decorator.
|
|
25
|
-
* Defines how a property should sync with HTML attributes and trigger updates.
|
|
26
|
-
*
|
|
27
|
-
* @interface PropertyOptions
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* ```typescript
|
|
31
|
-
* @property({ type: String, reflect: true })
|
|
32
|
-
* name = 'default';
|
|
33
|
-
*
|
|
34
|
-
* @property({ type: Number, attribute: 'data-count' })
|
|
35
|
-
* count = 0;
|
|
36
|
-
*
|
|
37
|
-
* @property({ type: Boolean })
|
|
38
|
-
* active = false;
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
export interface PropertyOptions {
|
|
42
|
-
/**
|
|
43
|
-
* The type constructor for converting attribute values to property values.
|
|
44
|
-
* Use String, Number, Boolean, Object, or Array.
|
|
45
|
-
* @type {PropertyType | oldPropertyType}
|
|
46
|
-
*/
|
|
47
|
-
type?: PropertyType | oldPropertyType;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The HTML attribute name to sync with, or false to disable attribute syncing.
|
|
51
|
-
* - `true` (default): Use kebab-case version of property name
|
|
52
|
-
* - `string`: Use specific attribute name
|
|
53
|
-
* - `false`: No attribute syncing
|
|
54
|
-
* @type {string | boolean}
|
|
55
|
-
*/
|
|
56
|
-
attribute?: string | boolean;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Whether to reflect property changes back to the HTML attribute.
|
|
60
|
-
* When true, setting the property will update the attribute.
|
|
61
|
-
* @type {boolean}
|
|
62
|
-
* @default false
|
|
63
|
-
*/
|
|
64
|
-
reflect?: boolean;
|
|
65
|
-
|
|
66
|
-
readonly?: boolean;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Custom converter for complex attribute/property transformations.
|
|
70
|
-
* @type {AttributeConverter<any>}
|
|
71
|
-
*/
|
|
72
|
-
converter?: AttributeConverter<any>;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* PropertyOptions extended with the property name.
|
|
77
|
-
* Used internally to track registered properties.
|
|
78
|
-
*
|
|
79
|
-
* @typedef {PropertyOptions & { name: string }} PropertyOptionsWithName
|
|
80
|
-
*/
|
|
81
|
-
export type PropertyOptionsWithName = PropertyOptions & {name: string};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Track which elements are currently updating attributes from property setters.
|
|
85
|
-
* Prevents infinite loops when reflection is enabled (property → attribute → property).
|
|
86
|
-
*
|
|
87
|
-
* @type {WeakSet<HTMLElement>}
|
|
88
|
-
* @private
|
|
89
|
-
*/
|
|
90
|
-
const settingAttributes = new WeakSet<HTMLElement>();
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Queue of pending attribute reflections for elements not yet connected to the DOM.
|
|
94
|
-
* Attribute reflections are deferred until the element is connected.
|
|
95
|
-
*
|
|
96
|
-
* @type {WeakMap<HTMLElement, Map<string, {value: any, type: any, converter?: AttributeConverter<any>}>>}
|
|
97
|
-
* @private
|
|
98
|
-
*/
|
|
99
|
-
const pendingAttributeReflections = new WeakMap<
|
|
100
|
-
HTMLElement,
|
|
101
|
-
Map<string, {value: any; type: any; converter?: AttributeConverter<any>}>
|
|
102
|
-
>();
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Queue of pending update callbacks for elements not yet connected to the DOM.
|
|
106
|
-
* Updates are deferred until the element is connected to avoid calling lifecycle
|
|
107
|
-
* methods on unattached elements.
|
|
108
|
-
*
|
|
109
|
-
* @type {WeakMap<HTMLElement, Array<{propertyKey: string, oldValue: any, newValue: any}>>}
|
|
110
|
-
* @private
|
|
111
|
-
*/
|
|
112
|
-
const pendingUpdates = new WeakMap<
|
|
113
|
-
HTMLElement,
|
|
114
|
-
Array<{propertyKey: string; oldValue: any; newValue: any}>
|
|
115
|
-
>();
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Checks if an element is currently updating its attributes from property setters.
|
|
119
|
-
* Used to prevent circular updates during property reflection.
|
|
120
|
-
*
|
|
121
|
-
* @param {HTMLElement} element - The element to check
|
|
122
|
-
* @returns {boolean} True if the element is currently setting attributes
|
|
123
|
-
*
|
|
124
|
-
* @example
|
|
125
|
-
* ```typescript
|
|
126
|
-
* if (!isSettingAttribute(this)) {
|
|
127
|
-
* // Safe to update property from attribute
|
|
128
|
-
* }
|
|
129
|
-
* ```
|
|
130
|
-
*/
|
|
131
|
-
export function isSettingAttribute(element: HTMLElement): boolean {
|
|
132
|
-
return settingAttributes.has(element);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Process any pending update callbacks for an element.
|
|
137
|
-
* Should be called when the element connects to the DOM to trigger
|
|
138
|
-
* deferred lifecycle callbacks.
|
|
139
|
-
*
|
|
140
|
-
* @param {any} element - The element with pending updates
|
|
141
|
-
* @returns {void}
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* ```typescript
|
|
145
|
-
* connectedCallback() {
|
|
146
|
-
* processPendingUpdates(this);
|
|
147
|
-
* }
|
|
148
|
-
* ```
|
|
149
|
-
*/
|
|
150
|
-
export function processPendingUpdates(element: any): void {
|
|
151
|
-
// Process pending attribute reflections first
|
|
152
|
-
const pendingReflections = pendingAttributeReflections.get(element);
|
|
153
|
-
if (pendingReflections) {
|
|
154
|
-
settingAttributes.add(element);
|
|
155
|
-
try {
|
|
156
|
-
pendingReflections.forEach((reflection, attributeName) => {
|
|
157
|
-
updateAttribute(
|
|
158
|
-
element,
|
|
159
|
-
attributeName,
|
|
160
|
-
reflection.value,
|
|
161
|
-
reflection.type,
|
|
162
|
-
reflection.converter,
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
} finally {
|
|
166
|
-
settingAttributes.delete(element);
|
|
167
|
-
}
|
|
168
|
-
pendingAttributeReflections.delete(element);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Process pending updated callbacks
|
|
172
|
-
const pending = pendingUpdates.get(element);
|
|
173
|
-
if (pending && element.updated) {
|
|
174
|
-
const pendingLength = pending.length;
|
|
175
|
-
let i = 0;
|
|
176
|
-
while (i < pendingLength) {
|
|
177
|
-
const update = pending[i];
|
|
178
|
-
element.updated(update.propertyKey, update.oldValue, update.newValue);
|
|
179
|
-
i = i + 1;
|
|
180
|
-
}
|
|
181
|
-
pendingUpdates.delete(element);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Interface for custom attribute conversion functions.
|
|
187
|
-
*
|
|
188
|
-
* @interface AttributeConverter
|
|
189
|
-
* @template T The type of the property value
|
|
190
|
-
* @description Provides custom conversion logic between HTML attribute strings
|
|
191
|
-
* and JavaScript property values for complex data types or custom formatting.
|
|
192
|
-
*/
|
|
193
|
-
export interface AttributeConverter<T> {
|
|
194
|
-
/**
|
|
195
|
-
* Converts an HTML attribute string value to a JavaScript property value.
|
|
196
|
-
*
|
|
197
|
-
* @param {string | null} value - The attribute value to convert
|
|
198
|
-
* @returns {T} The converted JavaScript value
|
|
199
|
-
* @optional
|
|
200
|
-
*
|
|
201
|
-
* @example
|
|
202
|
-
* ```typescript
|
|
203
|
-
* fromAttribute: (value: string | null) => {
|
|
204
|
-
* return value ? JSON.parse(value) : null;
|
|
205
|
-
* }
|
|
206
|
-
* ```
|
|
207
|
-
*/
|
|
208
|
-
fromAttribute?(value: string | null): T;
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Converts a JavaScript property value to an HTML attribute string.
|
|
212
|
-
*
|
|
213
|
-
* @param {T} value - The JavaScript value to convert
|
|
214
|
-
* @returns {string} The converted attribute string
|
|
215
|
-
* @optional
|
|
216
|
-
*
|
|
217
|
-
* @example
|
|
218
|
-
* ```typescript
|
|
219
|
-
* toAttribute: (value: object) => {
|
|
220
|
-
* return JSON.stringify(value);
|
|
221
|
-
* }
|
|
222
|
-
* ```
|
|
223
|
-
*/
|
|
224
|
-
toAttribute?(value: T): string;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Converts legacy string-based type to PropertyType constructor.
|
|
229
|
-
* Provides backward compatibility for old string type declarations.
|
|
230
|
-
*
|
|
231
|
-
* @param {PropertyType | oldPropertyType | undefined} type - The type to normalize
|
|
232
|
-
* @returns {PropertyType} The normalized PropertyType constructor (defaults to String)
|
|
233
|
-
* @private
|
|
234
|
-
*
|
|
235
|
-
* @example
|
|
236
|
-
* ```typescript
|
|
237
|
-
* normalizeType('string') // Returns String constructor
|
|
238
|
-
* normalizeType(Number) // Returns Number constructor
|
|
239
|
-
* ```
|
|
240
|
-
*/
|
|
241
|
-
function normalizeType(
|
|
242
|
-
type: PropertyType | oldPropertyType | undefined,
|
|
243
|
-
): PropertyType {
|
|
244
|
-
if (typeof type === "string") {
|
|
245
|
-
switch (type) {
|
|
246
|
-
case "string":
|
|
247
|
-
return String;
|
|
248
|
-
case "number":
|
|
249
|
-
return Number;
|
|
250
|
-
case "boolean":
|
|
251
|
-
return Boolean;
|
|
252
|
-
case "object":
|
|
253
|
-
return Object;
|
|
254
|
-
case "array":
|
|
255
|
-
return Array;
|
|
256
|
-
default:
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return type;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Converts a property value to the specified type.
|
|
265
|
-
* Uses custom converter if provided, otherwise performs standard type conversion.
|
|
266
|
-
*
|
|
267
|
-
* @param {any} value - The value to convert
|
|
268
|
-
* @param {PropertyType} [type] - The target type constructor
|
|
269
|
-
* @param {AttributeConverter<any>} [converter] - Optional custom converter
|
|
270
|
-
* @returns {any} The converted value
|
|
271
|
-
* @private
|
|
272
|
-
*
|
|
273
|
-
* @example
|
|
274
|
-
* ```typescript
|
|
275
|
-
* convertPropertyValue('42', Number) // Returns 42
|
|
276
|
-
* convertPropertyValue('true', Boolean) // Returns true
|
|
277
|
-
* convertPropertyValue('{"a":1}', Object) // Returns {a: 1}
|
|
278
|
-
* ```
|
|
279
|
-
*/
|
|
280
|
-
function convertPropertyValue(
|
|
281
|
-
value: any,
|
|
282
|
-
type?: PropertyType,
|
|
283
|
-
converter?: AttributeConverter<any>,
|
|
284
|
-
): any {
|
|
285
|
-
// Use custom converter if provided
|
|
286
|
-
if (converter && converter.fromAttribute && typeof value === "string") {
|
|
287
|
-
return converter.fromAttribute(value);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Preserve null/undefined
|
|
291
|
-
if (value === null || value === undefined) {
|
|
292
|
-
return value;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Standard type conversion
|
|
296
|
-
switch (type) {
|
|
297
|
-
case String:
|
|
298
|
-
return String(value);
|
|
299
|
-
case Number:
|
|
300
|
-
return Number(value);
|
|
301
|
-
case Boolean:
|
|
302
|
-
return Boolean(value);
|
|
303
|
-
case Object:
|
|
304
|
-
return typeof value === "object" ? value : JSON.parse(String(value));
|
|
305
|
-
case Array:
|
|
306
|
-
return Array.isArray(value) ? value : JSON.parse(String(value));
|
|
307
|
-
default:
|
|
308
|
-
return value;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Converts a camelCase string to kebab-case.
|
|
314
|
-
* Used to derive HTML attribute names from JavaScript property names.
|
|
315
|
-
*
|
|
316
|
-
* @param {string} str - The camelCase string to convert
|
|
317
|
-
* @returns {string} The kebab-case string
|
|
318
|
-
* @private
|
|
319
|
-
*
|
|
320
|
-
* @example
|
|
321
|
-
* ```typescript
|
|
322
|
-
* camelToKebabCase('myProperty') // Returns 'my-property'
|
|
323
|
-
* camelToKebabCase('isActive') // Returns 'is-active'
|
|
324
|
-
* ```
|
|
325
|
-
*/
|
|
326
|
-
function camelToKebabCase(str: string): string {
|
|
327
|
-
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Calculates the HTML attribute name based on property options.
|
|
332
|
-
*
|
|
333
|
-
* @param {string} propertyKey - The JavaScript property name
|
|
334
|
-
* @param {string | boolean | undefined} attribute - The attribute option from PropertyOptions
|
|
335
|
-
* @returns {string | undefined} The attribute name, or undefined if attribute syncing is disabled
|
|
336
|
-
* @private
|
|
337
|
-
*
|
|
338
|
-
* @example
|
|
339
|
-
* ```typescript
|
|
340
|
-
* getAttributeName('myProp', true) // Returns 'my-prop'
|
|
341
|
-
* getAttributeName('myProp', 'data-value') // Returns 'data-value'
|
|
342
|
-
* getAttributeName('myProp', false) // Returns undefined
|
|
343
|
-
* ```
|
|
344
|
-
*/
|
|
345
|
-
function getAttributeName(
|
|
346
|
-
propertyKey: string,
|
|
347
|
-
attribute: string | boolean | undefined,
|
|
348
|
-
): string | undefined {
|
|
349
|
-
if (attribute === false) return undefined;
|
|
350
|
-
if (attribute === true) return camelToKebabCase(propertyKey);
|
|
351
|
-
if (typeof attribute === "string") return attribute;
|
|
352
|
-
return undefined;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Updates the element's HTML attribute based on the property value.
|
|
357
|
-
* Handles special cases for Boolean, Object, and Array types.
|
|
358
|
-
*
|
|
359
|
-
* @param {HTMLElement} element - The element to update
|
|
360
|
-
* @param {string} attributeName - The attribute name to update
|
|
361
|
-
* @param {any} value - The property value to reflect
|
|
362
|
-
* @param {PropertyType} [type] - The property type constructor
|
|
363
|
-
* @param {AttributeConverter<any>} [converter] - Optional custom converter
|
|
364
|
-
* @returns {void}
|
|
365
|
-
* @private
|
|
366
|
-
*
|
|
367
|
-
* @example
|
|
368
|
-
* ```typescript
|
|
369
|
-
* updateAttribute(element, 'active', true, Boolean) // Sets empty attribute
|
|
370
|
-
* updateAttribute(element, 'count', 5, Number) // Sets attribute='5'
|
|
371
|
-
* ```
|
|
372
|
-
*/
|
|
373
|
-
function updateAttribute(
|
|
374
|
-
element: HTMLElement,
|
|
375
|
-
attributeName: string,
|
|
376
|
-
value: any,
|
|
377
|
-
type?: PropertyType,
|
|
378
|
-
converter?: AttributeConverter<any>,
|
|
379
|
-
): void {
|
|
380
|
-
if (type === Boolean) {
|
|
381
|
-
if (value) {
|
|
382
|
-
element.setAttribute(attributeName, "");
|
|
383
|
-
} else {
|
|
384
|
-
element.removeAttribute(attributeName);
|
|
385
|
-
}
|
|
386
|
-
} else if (type === Object || type === Array) {
|
|
387
|
-
if (converter && converter.toAttribute) {
|
|
388
|
-
element.setAttribute(attributeName, converter.toAttribute(value));
|
|
389
|
-
} else {
|
|
390
|
-
element.setAttribute(attributeName, JSON.stringify(value));
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
// Treat null/undefined as absence of attribute instead of the string "null"
|
|
394
|
-
if (value === null || value === undefined) {
|
|
395
|
-
element.removeAttribute(attributeName);
|
|
396
|
-
} else {
|
|
397
|
-
element.setAttribute(attributeName, String(value));
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Class field decorator that creates a reactive property with automatic attribute syncing.
|
|
404
|
-
*
|
|
405
|
-
* Features:
|
|
406
|
-
* - Automatic type conversion between attributes and properties
|
|
407
|
-
* - Optional property → attribute reflection
|
|
408
|
-
* - Custom converters for complex types
|
|
409
|
-
* - Automatic observedAttributes registration
|
|
410
|
-
* - Deferred updates for disconnected elements
|
|
411
|
-
* - Lifecycle integration (updated, shouldUpdate, renderNow)
|
|
412
|
-
*
|
|
413
|
-
* @decorator
|
|
414
|
-
* @param {PropertyOptions} [options={}] - Configuration options for the property
|
|
415
|
-
* @returns {(target: any, propertyKey: string) => void} The decorator function
|
|
416
|
-
*
|
|
417
|
-
* @example
|
|
418
|
-
* Basic usage with different types:
|
|
419
|
-
* ```typescript
|
|
420
|
-
* class MyElement extends BaseComponent {
|
|
421
|
-
* @property({ type: String })
|
|
422
|
-
* name = 'default';
|
|
423
|
-
*
|
|
424
|
-
* @property({ type: Number })
|
|
425
|
-
* count = 0;
|
|
426
|
-
*
|
|
427
|
-
* @property({ type: Boolean })
|
|
428
|
-
* active = false;
|
|
429
|
-
* }
|
|
430
|
-
* ```
|
|
431
|
-
*
|
|
432
|
-
* @example
|
|
433
|
-
* With reflection (property changes update attribute):
|
|
434
|
-
* ```typescript
|
|
435
|
-
* class MyElement extends BaseComponent {
|
|
436
|
-
* @property({ type: String, reflect: true })
|
|
437
|
-
* status = 'pending';
|
|
438
|
-
*
|
|
439
|
-
* updateStatus() {
|
|
440
|
-
* this.status = 'complete'; // Also updates status="complete" attribute
|
|
441
|
-
* }
|
|
442
|
-
* }
|
|
443
|
-
* ```
|
|
444
|
-
*
|
|
445
|
-
* @example
|
|
446
|
-
* Custom attribute name and converter:
|
|
447
|
-
* ```typescript
|
|
448
|
-
* class MyElement extends BaseComponent {
|
|
449
|
-
* @property({
|
|
450
|
-
* type: Object,
|
|
451
|
-
* attribute: 'data-config',
|
|
452
|
-
* converter: {
|
|
453
|
-
* fromAttribute: (value) => value ? JSON.parse(value) : {},
|
|
454
|
-
* toAttribute: (value) => JSON.stringify(value)
|
|
455
|
-
* }
|
|
456
|
-
* })
|
|
457
|
-
* config = {};
|
|
458
|
-
* }
|
|
459
|
-
* ```
|
|
460
|
-
*
|
|
461
|
-
* @example
|
|
462
|
-
* Property without attribute syncing:
|
|
463
|
-
* ```typescript
|
|
464
|
-
* class MyElement extends BaseComponent {
|
|
465
|
-
* @property({ attribute: false })
|
|
466
|
-
* internalState = null; // No attribute created or observed
|
|
467
|
-
* }
|
|
468
|
-
* ```
|
|
469
|
-
*/
|
|
470
|
-
export function property(
|
|
471
|
-
options: PropertyOptions = {},
|
|
472
|
-
): (target: any, propertyKey: string) => void {
|
|
473
|
-
const {type: rawType, attribute = true, reflect = false, converter, readonly = false} = options;
|
|
474
|
-
|
|
475
|
-
const type = normalizeType(rawType);
|
|
476
|
-
|
|
477
|
-
return (target: any, propertyKey: string): void => {
|
|
478
|
-
// Use WeakMap to store instance-specific values
|
|
479
|
-
const instanceMap = new WeakMap<any, any>();
|
|
480
|
-
const attributeName = getAttributeName(propertyKey, attribute);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Supported property type constructors for reactive properties.
|
|
3
|
+
* These constructors are used to convert attribute values to the correct JavaScript type.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {StringConstructor | NumberConstructor | BooleanConstructor | ObjectConstructor | ArrayConstructor} PropertyType
|
|
6
|
+
*/
|
|
7
|
+
type PropertyType =
|
|
8
|
+
| StringConstructor
|
|
9
|
+
| NumberConstructor
|
|
10
|
+
| BooleanConstructor
|
|
11
|
+
| ObjectConstructor
|
|
12
|
+
| ArrayConstructor;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Legacy string-based property types (deprecated).
|
|
16
|
+
* Maintained for backward compatibility. Use PropertyType constructors instead.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {('string' | 'number' | 'boolean' | 'object' | 'array')} oldPropertyType
|
|
19
|
+
* @deprecated Use PropertyType constructors (String, Number, Boolean, Object, Array) instead
|
|
20
|
+
*/
|
|
21
|
+
type oldPropertyType = "string" | "number" | "boolean" | "object" | "array";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration options for the @property decorator.
|
|
25
|
+
* Defines how a property should sync with HTML attributes and trigger updates.
|
|
26
|
+
*
|
|
27
|
+
* @interface PropertyOptions
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* @property({ type: String, reflect: true })
|
|
32
|
+
* name = 'default';
|
|
33
|
+
*
|
|
34
|
+
* @property({ type: Number, attribute: 'data-count' })
|
|
35
|
+
* count = 0;
|
|
36
|
+
*
|
|
37
|
+
* @property({ type: Boolean })
|
|
38
|
+
* active = false;
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export interface PropertyOptions {
|
|
42
|
+
/**
|
|
43
|
+
* The type constructor for converting attribute values to property values.
|
|
44
|
+
* Use String, Number, Boolean, Object, or Array.
|
|
45
|
+
* @type {PropertyType | oldPropertyType}
|
|
46
|
+
*/
|
|
47
|
+
type?: PropertyType | oldPropertyType;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The HTML attribute name to sync with, or false to disable attribute syncing.
|
|
51
|
+
* - `true` (default): Use kebab-case version of property name
|
|
52
|
+
* - `string`: Use specific attribute name
|
|
53
|
+
* - `false`: No attribute syncing
|
|
54
|
+
* @type {string | boolean}
|
|
55
|
+
*/
|
|
56
|
+
attribute?: string | boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether to reflect property changes back to the HTML attribute.
|
|
60
|
+
* When true, setting the property will update the attribute.
|
|
61
|
+
* @type {boolean}
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
reflect?: boolean;
|
|
65
|
+
|
|
66
|
+
readonly?: boolean;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Custom converter for complex attribute/property transformations.
|
|
70
|
+
* @type {AttributeConverter<any>}
|
|
71
|
+
*/
|
|
72
|
+
converter?: AttributeConverter<any>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* PropertyOptions extended with the property name.
|
|
77
|
+
* Used internally to track registered properties.
|
|
78
|
+
*
|
|
79
|
+
* @typedef {PropertyOptions & { name: string }} PropertyOptionsWithName
|
|
80
|
+
*/
|
|
81
|
+
export type PropertyOptionsWithName = PropertyOptions & {name: string};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Track which elements are currently updating attributes from property setters.
|
|
85
|
+
* Prevents infinite loops when reflection is enabled (property → attribute → property).
|
|
86
|
+
*
|
|
87
|
+
* @type {WeakSet<HTMLElement>}
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
const settingAttributes = new WeakSet<HTMLElement>();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Queue of pending attribute reflections for elements not yet connected to the DOM.
|
|
94
|
+
* Attribute reflections are deferred until the element is connected.
|
|
95
|
+
*
|
|
96
|
+
* @type {WeakMap<HTMLElement, Map<string, {value: any, type: any, converter?: AttributeConverter<any>}>>}
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
const pendingAttributeReflections = new WeakMap<
|
|
100
|
+
HTMLElement,
|
|
101
|
+
Map<string, {value: any; type: any; converter?: AttributeConverter<any>}>
|
|
102
|
+
>();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Queue of pending update callbacks for elements not yet connected to the DOM.
|
|
106
|
+
* Updates are deferred until the element is connected to avoid calling lifecycle
|
|
107
|
+
* methods on unattached elements.
|
|
108
|
+
*
|
|
109
|
+
* @type {WeakMap<HTMLElement, Array<{propertyKey: string, oldValue: any, newValue: any}>>}
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
const pendingUpdates = new WeakMap<
|
|
113
|
+
HTMLElement,
|
|
114
|
+
Array<{propertyKey: string; oldValue: any; newValue: any}>
|
|
115
|
+
>();
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Checks if an element is currently updating its attributes from property setters.
|
|
119
|
+
* Used to prevent circular updates during property reflection.
|
|
120
|
+
*
|
|
121
|
+
* @param {HTMLElement} element - The element to check
|
|
122
|
+
* @returns {boolean} True if the element is currently setting attributes
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* if (!isSettingAttribute(this)) {
|
|
127
|
+
* // Safe to update property from attribute
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function isSettingAttribute(element: HTMLElement): boolean {
|
|
132
|
+
return settingAttributes.has(element);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Process any pending update callbacks for an element.
|
|
137
|
+
* Should be called when the element connects to the DOM to trigger
|
|
138
|
+
* deferred lifecycle callbacks.
|
|
139
|
+
*
|
|
140
|
+
* @param {any} element - The element with pending updates
|
|
141
|
+
* @returns {void}
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* connectedCallback() {
|
|
146
|
+
* processPendingUpdates(this);
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function processPendingUpdates(element: any): void {
|
|
151
|
+
// Process pending attribute reflections first
|
|
152
|
+
const pendingReflections = pendingAttributeReflections.get(element);
|
|
153
|
+
if (pendingReflections) {
|
|
154
|
+
settingAttributes.add(element);
|
|
155
|
+
try {
|
|
156
|
+
pendingReflections.forEach((reflection, attributeName) => {
|
|
157
|
+
updateAttribute(
|
|
158
|
+
element,
|
|
159
|
+
attributeName,
|
|
160
|
+
reflection.value,
|
|
161
|
+
reflection.type,
|
|
162
|
+
reflection.converter,
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
} finally {
|
|
166
|
+
settingAttributes.delete(element);
|
|
167
|
+
}
|
|
168
|
+
pendingAttributeReflections.delete(element);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Process pending updated callbacks
|
|
172
|
+
const pending = pendingUpdates.get(element);
|
|
173
|
+
if (pending && element.updated) {
|
|
174
|
+
const pendingLength = pending.length;
|
|
175
|
+
let i = 0;
|
|
176
|
+
while (i < pendingLength) {
|
|
177
|
+
const update = pending[i];
|
|
178
|
+
element.updated(update.propertyKey, update.oldValue, update.newValue);
|
|
179
|
+
i = i + 1;
|
|
180
|
+
}
|
|
181
|
+
pendingUpdates.delete(element);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Interface for custom attribute conversion functions.
|
|
187
|
+
*
|
|
188
|
+
* @interface AttributeConverter
|
|
189
|
+
* @template T The type of the property value
|
|
190
|
+
* @description Provides custom conversion logic between HTML attribute strings
|
|
191
|
+
* and JavaScript property values for complex data types or custom formatting.
|
|
192
|
+
*/
|
|
193
|
+
export interface AttributeConverter<T> {
|
|
194
|
+
/**
|
|
195
|
+
* Converts an HTML attribute string value to a JavaScript property value.
|
|
196
|
+
*
|
|
197
|
+
* @param {string | null} value - The attribute value to convert
|
|
198
|
+
* @returns {T} The converted JavaScript value
|
|
199
|
+
* @optional
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* fromAttribute: (value: string | null) => {
|
|
204
|
+
* return value ? JSON.parse(value) : null;
|
|
205
|
+
* }
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
fromAttribute?(value: string | null): T;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Converts a JavaScript property value to an HTML attribute string.
|
|
212
|
+
*
|
|
213
|
+
* @param {T} value - The JavaScript value to convert
|
|
214
|
+
* @returns {string} The converted attribute string
|
|
215
|
+
* @optional
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* toAttribute: (value: object) => {
|
|
220
|
+
* return JSON.stringify(value);
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
toAttribute?(value: T): string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Converts legacy string-based type to PropertyType constructor.
|
|
229
|
+
* Provides backward compatibility for old string type declarations.
|
|
230
|
+
*
|
|
231
|
+
* @param {PropertyType | oldPropertyType | undefined} type - The type to normalize
|
|
232
|
+
* @returns {PropertyType} The normalized PropertyType constructor (defaults to String)
|
|
233
|
+
* @private
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* normalizeType('string') // Returns String constructor
|
|
238
|
+
* normalizeType(Number) // Returns Number constructor
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
function normalizeType(
|
|
242
|
+
type: PropertyType | oldPropertyType | undefined,
|
|
243
|
+
): PropertyType {
|
|
244
|
+
if (typeof type === "string") {
|
|
245
|
+
switch (type) {
|
|
246
|
+
case "string":
|
|
247
|
+
return String;
|
|
248
|
+
case "number":
|
|
249
|
+
return Number;
|
|
250
|
+
case "boolean":
|
|
251
|
+
return Boolean;
|
|
252
|
+
case "object":
|
|
253
|
+
return Object;
|
|
254
|
+
case "array":
|
|
255
|
+
return Array;
|
|
256
|
+
default:
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return type;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Converts a property value to the specified type.
|
|
265
|
+
* Uses custom converter if provided, otherwise performs standard type conversion.
|
|
266
|
+
*
|
|
267
|
+
* @param {any} value - The value to convert
|
|
268
|
+
* @param {PropertyType} [type] - The target type constructor
|
|
269
|
+
* @param {AttributeConverter<any>} [converter] - Optional custom converter
|
|
270
|
+
* @returns {any} The converted value
|
|
271
|
+
* @private
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* convertPropertyValue('42', Number) // Returns 42
|
|
276
|
+
* convertPropertyValue('true', Boolean) // Returns true
|
|
277
|
+
* convertPropertyValue('{"a":1}', Object) // Returns {a: 1}
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
function convertPropertyValue(
|
|
281
|
+
value: any,
|
|
282
|
+
type?: PropertyType,
|
|
283
|
+
converter?: AttributeConverter<any>,
|
|
284
|
+
): any {
|
|
285
|
+
// Use custom converter if provided
|
|
286
|
+
if (converter && converter.fromAttribute && typeof value === "string") {
|
|
287
|
+
return converter.fromAttribute(value);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Preserve null/undefined
|
|
291
|
+
if (value === null || value === undefined) {
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Standard type conversion
|
|
296
|
+
switch (type) {
|
|
297
|
+
case String:
|
|
298
|
+
return String(value);
|
|
299
|
+
case Number:
|
|
300
|
+
return Number(value);
|
|
301
|
+
case Boolean:
|
|
302
|
+
return Boolean(value);
|
|
303
|
+
case Object:
|
|
304
|
+
return typeof value === "object" ? value : JSON.parse(String(value));
|
|
305
|
+
case Array:
|
|
306
|
+
return Array.isArray(value) ? value : JSON.parse(String(value));
|
|
307
|
+
default:
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Converts a camelCase string to kebab-case.
|
|
314
|
+
* Used to derive HTML attribute names from JavaScript property names.
|
|
315
|
+
*
|
|
316
|
+
* @param {string} str - The camelCase string to convert
|
|
317
|
+
* @returns {string} The kebab-case string
|
|
318
|
+
* @private
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```typescript
|
|
322
|
+
* camelToKebabCase('myProperty') // Returns 'my-property'
|
|
323
|
+
* camelToKebabCase('isActive') // Returns 'is-active'
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
function camelToKebabCase(str: string): string {
|
|
327
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Calculates the HTML attribute name based on property options.
|
|
332
|
+
*
|
|
333
|
+
* @param {string} propertyKey - The JavaScript property name
|
|
334
|
+
* @param {string | boolean | undefined} attribute - The attribute option from PropertyOptions
|
|
335
|
+
* @returns {string | undefined} The attribute name, or undefined if attribute syncing is disabled
|
|
336
|
+
* @private
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* getAttributeName('myProp', true) // Returns 'my-prop'
|
|
341
|
+
* getAttributeName('myProp', 'data-value') // Returns 'data-value'
|
|
342
|
+
* getAttributeName('myProp', false) // Returns undefined
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
function getAttributeName(
|
|
346
|
+
propertyKey: string,
|
|
347
|
+
attribute: string | boolean | undefined,
|
|
348
|
+
): string | undefined {
|
|
349
|
+
if (attribute === false) return undefined;
|
|
350
|
+
if (attribute === true) return camelToKebabCase(propertyKey);
|
|
351
|
+
if (typeof attribute === "string") return attribute;
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Updates the element's HTML attribute based on the property value.
|
|
357
|
+
* Handles special cases for Boolean, Object, and Array types.
|
|
358
|
+
*
|
|
359
|
+
* @param {HTMLElement} element - The element to update
|
|
360
|
+
* @param {string} attributeName - The attribute name to update
|
|
361
|
+
* @param {any} value - The property value to reflect
|
|
362
|
+
* @param {PropertyType} [type] - The property type constructor
|
|
363
|
+
* @param {AttributeConverter<any>} [converter] - Optional custom converter
|
|
364
|
+
* @returns {void}
|
|
365
|
+
* @private
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* updateAttribute(element, 'active', true, Boolean) // Sets empty attribute
|
|
370
|
+
* updateAttribute(element, 'count', 5, Number) // Sets attribute='5'
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
function updateAttribute(
|
|
374
|
+
element: HTMLElement,
|
|
375
|
+
attributeName: string,
|
|
376
|
+
value: any,
|
|
377
|
+
type?: PropertyType,
|
|
378
|
+
converter?: AttributeConverter<any>,
|
|
379
|
+
): void {
|
|
380
|
+
if (type === Boolean) {
|
|
381
|
+
if (value) {
|
|
382
|
+
element.setAttribute(attributeName, "");
|
|
383
|
+
} else {
|
|
384
|
+
element.removeAttribute(attributeName);
|
|
385
|
+
}
|
|
386
|
+
} else if (type === Object || type === Array) {
|
|
387
|
+
if (converter && converter.toAttribute) {
|
|
388
|
+
element.setAttribute(attributeName, converter.toAttribute(value));
|
|
389
|
+
} else {
|
|
390
|
+
element.setAttribute(attributeName, JSON.stringify(value));
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
// Treat null/undefined as absence of attribute instead of the string "null"
|
|
394
|
+
if (value === null || value === undefined) {
|
|
395
|
+
element.removeAttribute(attributeName);
|
|
396
|
+
} else {
|
|
397
|
+
element.setAttribute(attributeName, String(value));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Class field decorator that creates a reactive property with automatic attribute syncing.
|
|
404
|
+
*
|
|
405
|
+
* Features:
|
|
406
|
+
* - Automatic type conversion between attributes and properties
|
|
407
|
+
* - Optional property → attribute reflection
|
|
408
|
+
* - Custom converters for complex types
|
|
409
|
+
* - Automatic observedAttributes registration
|
|
410
|
+
* - Deferred updates for disconnected elements
|
|
411
|
+
* - Lifecycle integration (updated, shouldUpdate, renderNow)
|
|
412
|
+
*
|
|
413
|
+
* @decorator
|
|
414
|
+
* @param {PropertyOptions} [options={}] - Configuration options for the property
|
|
415
|
+
* @returns {(target: any, propertyKey: string) => void} The decorator function
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* Basic usage with different types:
|
|
419
|
+
* ```typescript
|
|
420
|
+
* class MyElement extends BaseComponent {
|
|
421
|
+
* @property({ type: String })
|
|
422
|
+
* name = 'default';
|
|
423
|
+
*
|
|
424
|
+
* @property({ type: Number })
|
|
425
|
+
* count = 0;
|
|
426
|
+
*
|
|
427
|
+
* @property({ type: Boolean })
|
|
428
|
+
* active = false;
|
|
429
|
+
* }
|
|
430
|
+
* ```
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* With reflection (property changes update attribute):
|
|
434
|
+
* ```typescript
|
|
435
|
+
* class MyElement extends BaseComponent {
|
|
436
|
+
* @property({ type: String, reflect: true })
|
|
437
|
+
* status = 'pending';
|
|
438
|
+
*
|
|
439
|
+
* updateStatus() {
|
|
440
|
+
* this.status = 'complete'; // Also updates status="complete" attribute
|
|
441
|
+
* }
|
|
442
|
+
* }
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* Custom attribute name and converter:
|
|
447
|
+
* ```typescript
|
|
448
|
+
* class MyElement extends BaseComponent {
|
|
449
|
+
* @property({
|
|
450
|
+
* type: Object,
|
|
451
|
+
* attribute: 'data-config',
|
|
452
|
+
* converter: {
|
|
453
|
+
* fromAttribute: (value) => value ? JSON.parse(value) : {},
|
|
454
|
+
* toAttribute: (value) => JSON.stringify(value)
|
|
455
|
+
* }
|
|
456
|
+
* })
|
|
457
|
+
* config = {};
|
|
458
|
+
* }
|
|
459
|
+
* ```
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* Property without attribute syncing:
|
|
463
|
+
* ```typescript
|
|
464
|
+
* class MyElement extends BaseComponent {
|
|
465
|
+
* @property({ attribute: false })
|
|
466
|
+
* internalState = null; // No attribute created or observed
|
|
467
|
+
* }
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
export function property(
|
|
471
|
+
options: PropertyOptions = {},
|
|
472
|
+
): (target: any, propertyKey: string) => void {
|
|
473
|
+
const {type: rawType, attribute = true, reflect = false, converter, readonly = false} = options;
|
|
474
|
+
|
|
475
|
+
const type = normalizeType(rawType);
|
|
476
|
+
|
|
477
|
+
return (target: any, propertyKey: string): void => {
|
|
478
|
+
// Use WeakMap to store instance-specific values
|
|
479
|
+
const instanceMap = new WeakMap<any, any>();
|
|
480
|
+
const attributeName = getAttributeName(propertyKey, attribute);
|
|
481
|
+
|
|
482
|
+
// --- Patch lifecycle methods ONLY if defined in derived class ---
|
|
483
|
+
// Only patch once per class
|
|
484
|
+
if (!target.__p_elements_core_lifecycle_patch_applied) {
|
|
485
|
+
// Patch connectedCallback
|
|
486
|
+
if (Object.prototype.hasOwnProperty.call(target, 'connectedCallback') && typeof target.connectedCallback === 'function') {
|
|
487
|
+
const originalConnected = target.connectedCallback;
|
|
488
|
+
target.connectedCallback = function patchedConnectedCallback(...args: any[]) {
|
|
489
|
+
const PATCHED_FLAG = '__p_elements_core_pending_updates_called';
|
|
490
|
+
if (!this[PATCHED_FLAG]) {
|
|
491
|
+
let superCalled = false;
|
|
492
|
+
const origProcess = processPendingUpdates;
|
|
493
|
+
const self = this;
|
|
494
|
+
function wrappedProcessPendingUpdates(element: any) {
|
|
495
|
+
if (element === self) {
|
|
496
|
+
superCalled = true;
|
|
497
|
+
}
|
|
498
|
+
return origProcess(element);
|
|
499
|
+
}
|
|
500
|
+
(globalThis as any).__origProcessPendingUpdates = processPendingUpdates;
|
|
501
|
+
(globalThis as any).processPendingUpdates = wrappedProcessPendingUpdates;
|
|
502
|
+
try {
|
|
503
|
+
if (originalConnected) {
|
|
504
|
+
originalConnected.apply(this, args);
|
|
505
|
+
}
|
|
506
|
+
} finally {
|
|
507
|
+
(globalThis as any).processPendingUpdates = (globalThis as any).__origProcessPendingUpdates;
|
|
508
|
+
delete (globalThis as any).__origProcessPendingUpdates;
|
|
509
|
+
}
|
|
510
|
+
if (!superCalled) {
|
|
511
|
+
let baseProto = Object.getPrototypeOf(target);
|
|
512
|
+
let baseConnected = baseProto && baseProto.connectedCallback;
|
|
513
|
+
if (typeof baseConnected === 'function') {
|
|
514
|
+
baseConnected.apply(this, args);
|
|
515
|
+
if (typeof console !== 'undefined' && console.info) {
|
|
516
|
+
console.info(`[p-elements-core] connectedCallback: called base automatically for <${this.nodeName}>`);
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
processPendingUpdates(this);
|
|
520
|
+
if (typeof console !== 'undefined' && console.info) {
|
|
521
|
+
console.info(`[p-elements-core] connectedCallback: called processPendingUpdates automatically for <${this.nodeName}>`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
this[PATCHED_FLAG] = true;
|
|
526
|
+
} else if (originalConnected) {
|
|
527
|
+
originalConnected.apply(this, args);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Patch disconnectedCallback
|
|
533
|
+
if (Object.prototype.hasOwnProperty.call(target, 'disconnectedCallback') && typeof target.disconnectedCallback === 'function') {
|
|
534
|
+
const originalDisconnected = target.disconnectedCallback;
|
|
535
|
+
target.disconnectedCallback = function patchedDisconnectedCallback(...args: any[]) {
|
|
536
|
+
const PATCHED_FLAG = '__p_elements_core_disconnected_called';
|
|
537
|
+
if (!this[PATCHED_FLAG]) {
|
|
538
|
+
let superCalled = false;
|
|
539
|
+
const baseProto = Object.getPrototypeOf(target);
|
|
540
|
+
const baseDisconnected = baseProto && baseProto.disconnectedCallback;
|
|
541
|
+
if (originalDisconnected) {
|
|
542
|
+
originalDisconnected.apply(this, args);
|
|
543
|
+
superCalled = true;
|
|
544
|
+
}
|
|
545
|
+
if (!superCalled && typeof baseDisconnected === 'function') {
|
|
546
|
+
baseDisconnected.apply(this, args);
|
|
547
|
+
if (typeof console !== 'undefined' && console.info) {
|
|
548
|
+
console.info(`[p-elements-core] disconnectedCallback: called base automatically for <${this.nodeName}>`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
this[PATCHED_FLAG] = true;
|
|
552
|
+
} else if (originalDisconnected) {
|
|
553
|
+
originalDisconnected.apply(this, args);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Patch attributeChangedCallback
|
|
559
|
+
if (Object.prototype.hasOwnProperty.call(target, 'attributeChangedCallback') && typeof target.attributeChangedCallback === 'function') {
|
|
560
|
+
const originalAttributeChanged = target.attributeChangedCallback;
|
|
561
|
+
target.attributeChangedCallback = function patchedAttributeChangedCallback(...args: any[]) {
|
|
562
|
+
const PATCHED_FLAG = '__p_elements_core_attribute_changed_called';
|
|
563
|
+
if (!this[PATCHED_FLAG]) {
|
|
564
|
+
let superCalled = false;
|
|
565
|
+
const baseProto = Object.getPrototypeOf(target);
|
|
566
|
+
const baseAttributeChanged = baseProto && baseProto.attributeChangedCallback;
|
|
567
|
+
if (originalAttributeChanged) {
|
|
568
|
+
originalAttributeChanged.apply(this, args);
|
|
569
|
+
superCalled = true;
|
|
570
|
+
}
|
|
571
|
+
if (!superCalled && typeof baseAttributeChanged === 'function') {
|
|
572
|
+
baseAttributeChanged.apply(this, args);
|
|
573
|
+
if (typeof console !== 'undefined' && console.info) {
|
|
574
|
+
console.info(`[p-elements-core] attributeChangedCallback: called base automatically for <${this.nodeName}>`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
this[PATCHED_FLAG] = true;
|
|
578
|
+
} else if (originalAttributeChanged) {
|
|
579
|
+
originalAttributeChanged.apply(this, args);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
target.__p_elements_core_lifecycle_patch_applied = true;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
Object.defineProperty(target, propertyKey, {
|
|
588
|
+
get(this: any) {
|
|
589
|
+
// On first access, check if an HTML attribute exists and use that instead
|
|
590
|
+
if (
|
|
591
|
+
!instanceMap.has(this) &&
|
|
592
|
+
attributeName &&
|
|
593
|
+
this.hasAttribute &&
|
|
594
|
+
this.hasAttribute(attributeName)
|
|
595
|
+
) {
|
|
596
|
+
const attrValue = this.getAttribute(attributeName);
|
|
597
|
+
if (attrValue !== null) {
|
|
598
|
+
let convertedValue: any;
|
|
599
|
+
if (converter && converter.fromAttribute) {
|
|
600
|
+
convertedValue = converter.fromAttribute(attrValue);
|
|
601
|
+
} else if (type === Boolean) {
|
|
602
|
+
convertedValue = true;
|
|
603
|
+
} else if (type === Number) {
|
|
604
|
+
convertedValue = Number(attrValue);
|
|
605
|
+
} else {
|
|
606
|
+
convertedValue = attrValue;
|
|
607
|
+
}
|
|
608
|
+
instanceMap.set(this, convertedValue);
|
|
609
|
+
|
|
610
|
+
// Trigger update for initial HTML attribute
|
|
611
|
+
if (typeof this.renderNow === "function") {
|
|
612
|
+
this.renderNow();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return convertedValue;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Return the value if it exists, otherwise return default for type
|
|
620
|
+
if (instanceMap.has(this)) {
|
|
621
|
+
return instanceMap.get(this);
|
|
622
|
+
}
|
|
623
|
+
// Return type-appropriate default for undefined values
|
|
624
|
+
if (type === Boolean) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
return undefined;
|
|
628
|
+
},
|
|
629
|
+
set(this: any, value: any) {
|
|
630
|
+
// If readonly is true and value already exists, prevent setting
|
|
631
|
+
if (readonly && instanceMap.has(this)) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const oldValue = instanceMap.get(this);
|
|
636
|
+
let convertedValue: any;
|
|
637
|
+
let isRenderOnSet = false;
|
|
638
|
+
|
|
639
|
+
if ((type === null || type === undefined) && value !== oldValue) {
|
|
640
|
+
isRenderOnSet = true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
// On first set, check if an HTML attribute exists and use that instead of the default
|
|
645
|
+
if (
|
|
646
|
+
!instanceMap.has(this) &&
|
|
647
|
+
attributeName &&
|
|
648
|
+
this.hasAttribute &&
|
|
649
|
+
this.hasAttribute(attributeName)
|
|
650
|
+
) {
|
|
651
|
+
|
|
652
|
+
const attrValue = this.getAttribute(attributeName);
|
|
653
|
+
if (attrValue !== null) {
|
|
654
|
+
if (converter && converter.fromAttribute) {
|
|
655
|
+
convertedValue = converter.fromAttribute(attrValue);
|
|
656
|
+
} else if (type === Boolean) {
|
|
657
|
+
convertedValue = true;
|
|
658
|
+
} else if (type === Number) {
|
|
659
|
+
convertedValue = Number(attrValue);
|
|
660
|
+
} else {
|
|
661
|
+
convertedValue = attrValue;
|
|
662
|
+
}
|
|
663
|
+
instanceMap.set(this, convertedValue);
|
|
664
|
+
|
|
665
|
+
// Trigger update for initial HTML attribute only if connected
|
|
666
|
+
if (this.isConnected) {
|
|
667
|
+
|
|
668
|
+
if (typeof this.renderNow === "function") {
|
|
669
|
+
this.renderNow();
|
|
670
|
+
}
|
|
671
|
+
// Call updated callback
|
|
672
|
+
if (this.updated) {
|
|
673
|
+
this.updated(propertyKey, oldValue, convertedValue);
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
|
|
677
|
+
// Queue for later when element connects
|
|
678
|
+
if (this.updated) {
|
|
679
|
+
if (!pendingUpdates.has(this)) {
|
|
680
|
+
pendingUpdates.set(this, []);
|
|
681
|
+
}
|
|
682
|
+
pendingUpdates
|
|
683
|
+
.get(this)!
|
|
684
|
+
.push({propertyKey, oldValue, newValue: convertedValue});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (isRenderOnSet) {
|
|
692
|
+
convertedValue = value;
|
|
693
|
+
} else {
|
|
694
|
+
convertedValue = convertPropertyValue(value, type, converter);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (oldValue === convertedValue) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const shouldUpdate = this.shouldUpdate
|
|
702
|
+
? this.shouldUpdate(propertyKey, oldValue, convertedValue)
|
|
703
|
+
: true;
|
|
704
|
+
|
|
705
|
+
instanceMap.set(this, shouldUpdate ? convertedValue : oldValue);
|
|
706
|
+
|
|
707
|
+
// Update attribute if reflect is enabled
|
|
708
|
+
if (reflect && attributeName !== undefined) {
|
|
709
|
+
if (this.isConnected) {
|
|
710
|
+
// Element is connected, update attribute immediately
|
|
711
|
+
settingAttributes.add(this);
|
|
712
|
+
try {
|
|
713
|
+
updateAttribute(
|
|
714
|
+
this,
|
|
715
|
+
attributeName,
|
|
716
|
+
shouldUpdate ? convertedValue : oldValue,
|
|
717
|
+
type,
|
|
718
|
+
converter,
|
|
719
|
+
);
|
|
720
|
+
} finally {
|
|
721
|
+
settingAttributes.delete(this);
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
// Element not connected yet, queue for later
|
|
725
|
+
if (!pendingAttributeReflections.has(this)) {
|
|
726
|
+
pendingAttributeReflections.set(this, new Map());
|
|
727
|
+
}
|
|
728
|
+
pendingAttributeReflections.get(this)!.set(attributeName, {
|
|
729
|
+
value: shouldUpdate ? convertedValue : oldValue,
|
|
730
|
+
type,
|
|
731
|
+
converter,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Trigger update only if connected
|
|
739
|
+
if (this.isConnected) {
|
|
740
|
+
if (typeof this.renderNow === "function") {
|
|
741
|
+
this.renderNow();
|
|
742
|
+
}
|
|
743
|
+
// Call updated callback
|
|
744
|
+
if (this.updated) {
|
|
745
|
+
this.updated(propertyKey, oldValue, convertedValue);
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
// Queue updated callback for later when element connects
|
|
749
|
+
if (this.updated) {
|
|
750
|
+
if (!pendingUpdates.has(this)) {
|
|
751
|
+
pendingUpdates.set(this, []);
|
|
752
|
+
}
|
|
753
|
+
pendingUpdates
|
|
754
|
+
.get(this)!
|
|
755
|
+
.push({propertyKey, oldValue, newValue: convertedValue});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Call renderNow if @RenderOnSet is used and value changed
|
|
760
|
+
if (isRenderOnSet && typeof this.renderNow === "function" ) {
|
|
761
|
+
this.renderNow();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
},
|
|
765
|
+
configurable: true,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Register property info for attribute change detection
|
|
769
|
+
if (!target.constructor._propertyInfo) {
|
|
770
|
+
target.constructor._propertyInfo = new Map<string, PropertyOptions>();
|
|
771
|
+
}
|
|
772
|
+
target.constructor._propertyInfo.set(propertyKey, {
|
|
773
|
+
...options,
|
|
774
|
+
name: propertyKey,
|
|
775
|
+
attribute: attributeName,
|
|
776
|
+
type,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// Register observed attributes
|
|
780
|
+
if (attributeName) {
|
|
781
|
+
if (!target.constructor.observedAttributes) {
|
|
782
|
+
target.constructor.observedAttributes = [];
|
|
783
|
+
}
|
|
784
|
+
if (!target.constructor.observedAttributes.includes(attributeName)) {
|
|
785
|
+
target.constructor.observedAttributes.push(attributeName);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|