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.
Files changed (78) hide show
  1. package/.editorconfig +17 -17
  2. package/.gitlab-ci.yml +18 -18
  3. package/CHANGELOG.md +201 -0
  4. package/demo/sample.js +1 -1
  5. package/demo/screen.css +16 -16
  6. package/dist/p-elements-core-modern.js +1 -1
  7. package/dist/p-elements-core.js +1 -1
  8. package/docs/package-lock.json +6897 -6897
  9. package/docs/package.json +27 -27
  10. package/docs/src/404.md +8 -8
  11. package/docs/src/_data/demos/hello-world/hello-world.tsx +35 -35
  12. package/docs/src/_data/demos/hello-world/index.html +10 -10
  13. package/docs/src/_data/demos/hello-world/project.json +7 -7
  14. package/docs/src/_data/demos/timer/demo-timer.tsx +120 -120
  15. package/docs/src/_data/demos/timer/icons.tsx +62 -62
  16. package/docs/src/_data/demos/timer/index.html +12 -12
  17. package/docs/src/_data/demos/timer/project.json +8 -8
  18. package/docs/src/_data/global.js +13 -13
  19. package/docs/src/_data/helpers.js +19 -19
  20. package/docs/src/_includes/layouts/base.njk +30 -30
  21. package/docs/src/_includes/layouts/playground.njk +40 -40
  22. package/docs/src/_includes/partials/app-header.njk +8 -8
  23. package/docs/src/_includes/partials/head.njk +14 -14
  24. package/docs/src/_includes/partials/nav.njk +19 -19
  25. package/docs/src/_includes/partials/top-nav.njk +51 -51
  26. package/docs/src/documentation/custom-element.md +221 -221
  27. package/docs/src/documentation/decorators/bind.md +71 -71
  28. package/docs/src/documentation/decorators/custom-element-config.md +63 -63
  29. package/docs/src/documentation/decorators/property.md +83 -83
  30. package/docs/src/documentation/decorators/query.md +66 -66
  31. package/docs/src/documentation/decorators/render-property-on-set.md +60 -60
  32. package/docs/src/documentation/decorators.md +9 -9
  33. package/docs/src/documentation/reactive-properties.md +53 -53
  34. package/docs/src/index.d.ts +25 -25
  35. package/docs/src/index.md +3 -3
  36. package/docs/src/scripts/components/app-mode-switch/app-mode-switch.css +78 -78
  37. package/docs/src/scripts/components/app-mode-switch/app-mode-switch.tsx +166 -166
  38. package/docs/src/scripts/components/app-playground/app-playground.tsx +189 -189
  39. package/docs/tsconfig.json +22 -22
  40. package/package.json +9 -2
  41. package/readme.md +206 -206
  42. package/src/custom-element-controller.test.ts +226 -0
  43. package/src/custom-element-controller.ts +31 -31
  44. package/src/custom-element.test.ts +906 -0
  45. package/src/custom-element.ts +17 -1
  46. package/src/custom-style-element.ts +4 -1
  47. package/src/decorators/bind.test.ts +163 -0
  48. package/src/decorators/bind.ts +46 -46
  49. package/src/decorators/custom-element-config.ts +17 -17
  50. package/src/decorators/property.test.ts +279 -0
  51. package/src/decorators/property.ts +789 -684
  52. package/src/decorators/query.test.ts +146 -0
  53. package/src/decorators/query.ts +12 -12
  54. package/src/decorators/render-property-on-set.ts +3 -3
  55. package/src/helpers/css.test.ts +150 -0
  56. package/src/helpers/css.ts +71 -71
  57. package/src/maquette/cache.test.ts +150 -0
  58. package/src/maquette/cache.ts +35 -35
  59. package/src/maquette/dom.test.ts +263 -0
  60. package/src/maquette/dom.ts +115 -115
  61. package/src/maquette/h.test.ts +165 -0
  62. package/src/maquette/h.ts +100 -100
  63. package/src/maquette/index.ts +12 -12
  64. package/src/maquette/interfaces.ts +536 -536
  65. package/src/maquette/jsx.ts +61 -61
  66. package/src/maquette/mapping.test.ts +294 -0
  67. package/src/maquette/mapping.ts +56 -56
  68. package/src/maquette/maquette.test.ts +493 -0
  69. package/src/maquette/projection.test.ts +366 -0
  70. package/src/maquette/projection.ts +666 -666
  71. package/src/maquette/projector.test.ts +351 -0
  72. package/src/maquette/projector.ts +200 -200
  73. package/src/sample/mixin/highlight.tsx +33 -33
  74. package/src/test-setup.ts +85 -0
  75. package/src/test-utils.ts +223 -0
  76. package/tsconfig.json +1 -0
  77. package/vitest.config.ts +41 -0
  78. 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
- Object.defineProperty(target, propertyKey, {
483
- get(this: any) {
484
- // On first access, check if an HTML attribute exists and use that instead
485
- if (
486
- !instanceMap.has(this) &&
487
- attributeName &&
488
- this.hasAttribute &&
489
- this.hasAttribute(attributeName)
490
- ) {
491
- const attrValue = this.getAttribute(attributeName);
492
- if (attrValue !== null) {
493
- let convertedValue: any;
494
- if (converter && converter.fromAttribute) {
495
- convertedValue = converter.fromAttribute(attrValue);
496
- } else if (type === Boolean) {
497
- convertedValue = true;
498
- } else if (type === Number) {
499
- convertedValue = Number(attrValue);
500
- } else {
501
- convertedValue = attrValue;
502
- }
503
- instanceMap.set(this, convertedValue);
504
-
505
- // Trigger update for initial HTML attribute
506
- if (typeof this.renderNow === "function") {
507
- this.renderNow();
508
- }
509
-
510
- return convertedValue;
511
- }
512
- }
513
-
514
- // Return the value if it exists, otherwise return default for type
515
- if (instanceMap.has(this)) {
516
- return instanceMap.get(this);
517
- }
518
- // Return type-appropriate default for undefined values
519
- if (type === Boolean) {
520
- return false;
521
- }
522
- return undefined;
523
- },
524
- set(this: any, value: any) {
525
- // If readonly is true and value already exists, prevent setting
526
- if (readonly && instanceMap.has(this)) {
527
- return;
528
- }
529
-
530
- const oldValue = instanceMap.get(this);
531
- let convertedValue: any;
532
- let isRenderOnSet = false;
533
-
534
- if ((type === null || type === undefined) && value !== oldValue) {
535
- isRenderOnSet = true;
536
- }
537
-
538
-
539
- // On first set, check if an HTML attribute exists and use that instead of the default
540
- if (
541
- !instanceMap.has(this) &&
542
- attributeName &&
543
- this.hasAttribute &&
544
- this.hasAttribute(attributeName)
545
- ) {
546
-
547
- const attrValue = this.getAttribute(attributeName);
548
- if (attrValue !== null) {
549
- if (converter && converter.fromAttribute) {
550
- convertedValue = converter.fromAttribute(attrValue);
551
- } else if (type === Boolean) {
552
- convertedValue = true;
553
- } else if (type === Number) {
554
- convertedValue = Number(attrValue);
555
- } else {
556
- convertedValue = attrValue;
557
- }
558
- instanceMap.set(this, convertedValue);
559
-
560
- // Trigger update for initial HTML attribute only if connected
561
- if (this.isConnected) {
562
-
563
- if (typeof this.renderNow === "function") {
564
- this.renderNow();
565
- }
566
- // Call updated callback
567
- if (this.updated) {
568
- this.updated(propertyKey, oldValue, convertedValue);
569
- }
570
- } else {
571
-
572
- // Queue for later when element connects
573
- if (this.updated) {
574
- if (!pendingUpdates.has(this)) {
575
- pendingUpdates.set(this, []);
576
- }
577
- pendingUpdates
578
- .get(this)!
579
- .push({propertyKey, oldValue, newValue: convertedValue});
580
- }
581
- }
582
-
583
- return;
584
- }
585
- }
586
- if (isRenderOnSet) {
587
- convertedValue = value;
588
- } else {
589
- convertedValue = convertPropertyValue(value, type, converter);
590
- }
591
-
592
- if (oldValue === convertedValue) {
593
- return;
594
- }
595
-
596
- const shouldUpdate = this.shouldUpdate
597
- ? this.shouldUpdate(propertyKey, oldValue, convertedValue)
598
- : true;
599
-
600
- instanceMap.set(this, shouldUpdate ? convertedValue : oldValue);
601
-
602
- // Update attribute if reflect is enabled
603
- if (reflect && attributeName !== undefined) {
604
- if (this.isConnected) {
605
- // Element is connected, update attribute immediately
606
- settingAttributes.add(this);
607
- try {
608
- updateAttribute(
609
- this,
610
- attributeName,
611
- shouldUpdate ? convertedValue : oldValue,
612
- type,
613
- converter,
614
- );
615
- } finally {
616
- settingAttributes.delete(this);
617
- }
618
- } else {
619
- // Element not connected yet, queue for later
620
- if (!pendingAttributeReflections.has(this)) {
621
- pendingAttributeReflections.set(this, new Map());
622
- }
623
- pendingAttributeReflections.get(this)!.set(attributeName, {
624
- value: shouldUpdate ? convertedValue : oldValue,
625
- type,
626
- converter,
627
- });
628
- }
629
-
630
-
631
- }
632
-
633
- // Trigger update only if connected
634
- if (this.isConnected) {
635
- if (typeof this.renderNow === "function") {
636
- this.renderNow();
637
- }
638
- // Call updated callback
639
- if (this.updated) {
640
- this.updated(propertyKey, oldValue, convertedValue);
641
- }
642
- } else {
643
- // Queue updated callback for later when element connects
644
- if (this.updated) {
645
- if (!pendingUpdates.has(this)) {
646
- pendingUpdates.set(this, []);
647
- }
648
- pendingUpdates
649
- .get(this)!
650
- .push({propertyKey, oldValue, newValue: convertedValue});
651
- }
652
- }
653
-
654
- // Call renderNow if @RenderOnSet is used and value changed
655
- if (isRenderOnSet && typeof this.renderNow === "function" ) {
656
- this.renderNow();
657
- }
658
-
659
- },
660
- configurable: true,
661
- });
662
-
663
- // Register property info for attribute change detection
664
- if (!target.constructor._propertyInfo) {
665
- target.constructor._propertyInfo = new Map<string, PropertyOptions>();
666
- }
667
- target.constructor._propertyInfo.set(propertyKey, {
668
- ...options,
669
- name: propertyKey,
670
- attribute: attributeName,
671
- type,
672
- });
673
-
674
- // Register observed attributes
675
- if (attributeName) {
676
- if (!target.constructor.observedAttributes) {
677
- target.constructor.observedAttributes = [];
678
- }
679
- if (!target.constructor.observedAttributes.includes(attributeName)) {
680
- target.constructor.observedAttributes.push(attributeName);
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
+ }