p-elements-core 1.2.32-rc1 → 1.2.32-rc11
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/CHANGELOG.md +201 -0
- package/demo/sample.js +1 -1
- package/demo/theme.css +1 -0
- package/dist/p-elements-core-modern.js +1 -1
- package/dist/p-elements-core.js +1 -1
- package/index.html +15 -2
- package/p-elements-core.d.ts +11 -1
- package/package.json +10 -3
- package/src/custom-element-controller.test.ts +226 -0
- package/src/custom-element.test.ts +906 -0
- package/src/custom-element.ts +74 -17
- package/src/custom-style-element.ts +4 -1
- package/src/decorators/bind.test.ts +163 -0
- package/src/decorators/property.test.ts +279 -0
- package/src/decorators/property.ts +176 -10
- package/src/decorators/query.test.ts +146 -0
- package/src/helpers/css.test.ts +150 -0
- package/src/maquette/cache.test.ts +150 -0
- package/src/maquette/dom.test.ts +263 -0
- package/src/maquette/h.test.ts +165 -0
- package/src/maquette/mapping.test.ts +294 -0
- package/src/maquette/maquette.test.ts +493 -0
- package/src/maquette/projection.test.ts +366 -0
- package/src/maquette/projector.test.ts +351 -0
- package/src/maquette/projector.ts +6 -1
- package/src/sample/sample.tsx +167 -8
- 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
package/src/custom-element.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
|
|
7
7
|
import { ICustomElementController } from "./custom-element-controller";
|
|
8
8
|
import { Projector, VNode } from "./maquette/interfaces";
|
|
9
|
+
import { replaceApplyToCssVars } from "./helpers/css";
|
|
9
10
|
|
|
10
11
|
export type ElementProjectorMode = "append" | "merge" | "replace";
|
|
11
12
|
|
|
@@ -70,7 +71,7 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
70
71
|
|
|
71
72
|
#internalsObjectUntilAttached: object | null;
|
|
72
73
|
|
|
73
|
-
#controllers: ICustomElementController[] = [];
|
|
74
|
+
readonly #controllers: ICustomElementController[] = [];
|
|
74
75
|
|
|
75
76
|
/** Promise that resolves when the current update is complete */
|
|
76
77
|
#updatePromise: Promise<void> | null = null;
|
|
@@ -124,6 +125,20 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
124
125
|
return this.#connected;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Array of reactive property metadata for the component, indexed by property name.
|
|
130
|
+
* Used by @property decorator and attributeChangedCallback
|
|
131
|
+
*
|
|
132
|
+
* @returns {readonly PropertyOptionsWithName[]} Array of property metadata
|
|
133
|
+
*/
|
|
134
|
+
get properties(): readonly PropertyOptionsWithName[] {
|
|
135
|
+
const ctor = this.constructor as ComponentConstructor;
|
|
136
|
+
if (!ctor._propertyInfo) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
return Array.from(ctor._propertyInfo.values());
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
/**
|
|
128
143
|
* Promise that resolves when the component has finished updating
|
|
129
144
|
* and rendering to the DOM.
|
|
@@ -216,7 +231,7 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
216
231
|
* @public
|
|
217
232
|
*/
|
|
218
233
|
renderNow(): void {
|
|
219
|
-
if (!this.shadowRoot) {
|
|
234
|
+
if (this.#useShadowRoot && !this.shadowRoot) {
|
|
220
235
|
return;
|
|
221
236
|
}
|
|
222
237
|
|
|
@@ -226,8 +241,6 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
226
241
|
this.#updateResolve = resolve;
|
|
227
242
|
});
|
|
228
243
|
}
|
|
229
|
-
// this.#projector.
|
|
230
|
-
// render(this.shadowRoot, this.render());
|
|
231
244
|
this.#projector?.renderNow();
|
|
232
245
|
|
|
233
246
|
// Call updated() lifecycle hook after DOM updates are complete
|
|
@@ -299,6 +312,7 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
299
312
|
if (!this.#cssSheet) {
|
|
300
313
|
this.#cssSheet = new CSSStyleSheet();
|
|
301
314
|
}
|
|
315
|
+
style = this.#polyfillCssApply();
|
|
302
316
|
this.#cssSheet.replaceSync(style);
|
|
303
317
|
if (this.#isSheetAdopted) {
|
|
304
318
|
return;
|
|
@@ -320,6 +334,7 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
320
334
|
} else {
|
|
321
335
|
this.#linkElement = document.createElement("link");
|
|
322
336
|
this.#linkElement.rel = "stylesheet";
|
|
337
|
+
style = this.#polyfillCssApply();
|
|
323
338
|
this.#linkElement.href = URL.createObjectURL(
|
|
324
339
|
new Blob([style], { type: "text/css" }),
|
|
325
340
|
);
|
|
@@ -357,6 +372,9 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
357
372
|
this.#initStylesheet(styleElement.textContent);
|
|
358
373
|
styleElement.remove();
|
|
359
374
|
}
|
|
375
|
+
window.addEventListener("updatecssapply", () => {
|
|
376
|
+
this.#polyfillCssApply();
|
|
377
|
+
});
|
|
360
378
|
|
|
361
379
|
return fragment;
|
|
362
380
|
}
|
|
@@ -368,10 +386,14 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
368
386
|
this.addStylesheetToRootNode(css, root);
|
|
369
387
|
}
|
|
370
388
|
|
|
371
|
-
protected createProjector(
|
|
389
|
+
protected async createProjector(
|
|
372
390
|
element: Element,
|
|
373
391
|
render: () => VNode,
|
|
374
392
|
): Promise<Projector> {
|
|
393
|
+
return this.#createProjector(element, render);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#createProjector(element: Element, render: () => VNode): Promise<Projector> {
|
|
375
397
|
return new Promise<Projector>((resolve, reject) => {
|
|
376
398
|
let projector: Projector;
|
|
377
399
|
const mode = this.#projectorMode ? this.#projectorMode : "append";
|
|
@@ -381,7 +403,7 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
381
403
|
if (eventName === "renderStart" || eventName === "renderDone") {
|
|
382
404
|
this.#invokeRenderLifecycleFn(eventName);
|
|
383
405
|
}
|
|
384
|
-
}
|
|
406
|
+
},
|
|
385
407
|
});
|
|
386
408
|
projector[mode](element, render.bind(this));
|
|
387
409
|
this.#projector = projector;
|
|
@@ -396,19 +418,29 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
396
418
|
if (typeof (this as any).init === "function") {
|
|
397
419
|
(this as any).init();
|
|
398
420
|
this.#controllers.forEach((controller) => {
|
|
399
|
-
controller?.init
|
|
421
|
+
if (controller?.init) {
|
|
422
|
+
controller.init();
|
|
423
|
+
}
|
|
400
424
|
});
|
|
401
425
|
}
|
|
402
426
|
}
|
|
403
427
|
|
|
404
428
|
#invokeRenderLifecycleFn(eventName: string) {
|
|
405
|
-
if (this[eventName]){
|
|
406
|
-
this[eventName](
|
|
429
|
+
if (this[eventName]) {
|
|
430
|
+
this[eventName](
|
|
431
|
+
eventName === "renderStart"
|
|
432
|
+
? this.#isFirstRenderStart
|
|
433
|
+
: this.#isFirstRenderDone,
|
|
434
|
+
);
|
|
407
435
|
}
|
|
408
436
|
const controllerEventName = `host${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
|
|
409
437
|
this.#controllers.forEach((controller) => {
|
|
410
438
|
if (controller[controllerEventName]) {
|
|
411
|
-
controller[controllerEventName](
|
|
439
|
+
controller[controllerEventName](
|
|
440
|
+
eventName === "renderStart"
|
|
441
|
+
? this.#isFirstRenderStart
|
|
442
|
+
: this.#isFirstRenderDone,
|
|
443
|
+
);
|
|
412
444
|
}
|
|
413
445
|
});
|
|
414
446
|
if (eventName === "renderStart") {
|
|
@@ -465,14 +497,33 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
465
497
|
const div = document.createElement("div");
|
|
466
498
|
this.shadowRoot.appendChild(div);
|
|
467
499
|
requestAnimationFrame(() => {
|
|
468
|
-
this
|
|
500
|
+
this.#createProjector(div, (this as any).render).then(() => {
|
|
469
501
|
this.#upgradeProperties();
|
|
470
|
-
})
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
window.addEventListener("updatecssapply", () => {
|
|
505
|
+
this.#polyfillCssApply();
|
|
471
506
|
});
|
|
472
507
|
}
|
|
473
508
|
}
|
|
474
509
|
|
|
475
|
-
|
|
510
|
+
#polyfillCssApply(): string {
|
|
511
|
+
let style = replaceApplyToCssVars(this.#cssText);
|
|
512
|
+
if (this.#cssText !== style) {
|
|
513
|
+
this.#cssText = style;
|
|
514
|
+
if (this.#hasAdoptedStyleSheetsSupport && this.#cssSheet) {
|
|
515
|
+
this.#cssSheet.replaceSync(style);
|
|
516
|
+
} else if (!this.#hasAdoptedStyleSheetsSupport) {
|
|
517
|
+
if (this.#linkElement) {
|
|
518
|
+
URL.revokeObjectURL(this.#linkElement.href);
|
|
519
|
+
}
|
|
520
|
+
this.#linkElement.href = URL.createObjectURL(
|
|
521
|
+
new Blob([style], { type: "text/css" }),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return style;
|
|
526
|
+
}
|
|
476
527
|
|
|
477
528
|
#initStylesheet(style: string) {
|
|
478
529
|
this.#cssText = style;
|
|
@@ -501,6 +552,9 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
501
552
|
* Initializes controllers, processes pending property updates, and renders.
|
|
502
553
|
*/
|
|
503
554
|
connectedCallback(): void {
|
|
555
|
+
if (this.#connected) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
504
558
|
this.#connected = true;
|
|
505
559
|
let i = 0;
|
|
506
560
|
const controllersLength = this.#controllers.length;
|
|
@@ -521,6 +575,9 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
521
575
|
* Marks the component as disconnected.
|
|
522
576
|
*/
|
|
523
577
|
disconnectedCallback(): void {
|
|
578
|
+
if (this.#connected === false) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
524
581
|
this.#connected = false;
|
|
525
582
|
let i = 0;
|
|
526
583
|
const controllersLength = this.#controllers.length;
|
|
@@ -539,21 +596,21 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
539
596
|
* Prevents infinite loops by skipping updates initiated by property setters
|
|
540
597
|
*
|
|
541
598
|
* @param {string} name - The name of the changed attribute
|
|
542
|
-
* @param {string | null}
|
|
599
|
+
* @param {string | null} oldValue - The previous attribute value (unused)
|
|
543
600
|
* @param {string | null} newValue - The new attribute value
|
|
544
601
|
*
|
|
545
602
|
* @private
|
|
546
603
|
*/
|
|
547
604
|
attributeChangedCallback(
|
|
548
605
|
name: string,
|
|
549
|
-
|
|
606
|
+
oldValue: string | null,
|
|
550
607
|
newValue: string | null,
|
|
551
608
|
): void {
|
|
552
609
|
// Skip if this attribute change came from a property setter (prevent infinite loop)
|
|
553
|
-
if (
|
|
610
|
+
// Also skip if the value didn't actually change (some browsers may call this callback even if the value is the same)
|
|
611
|
+
if (isSettingAttribute(this) || newValue === oldValue) {
|
|
554
612
|
return;
|
|
555
613
|
}
|
|
556
|
-
|
|
557
614
|
const ctor = this.constructor as ComponentConstructor;
|
|
558
615
|
if (!ctor?._propertyInfo) {
|
|
559
616
|
return;
|
|
@@ -28,4 +28,7 @@ customElements.whenDefined("custom-style").then(() => {
|
|
|
28
28
|
document.body.parentElement.classList.add("custom-style-defined");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// Only define if not already defined
|
|
32
|
+
if (!customElements.get("custom-style")) {
|
|
33
|
+
customElements.define("custom-style", CustomStyleElement, { extends: "link" });
|
|
34
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for @bind decorator
|
|
3
|
+
* Covers method binding to preserve 'this' context
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import '../test-setup.js';
|
|
8
|
+
import { bind } from './bind.js';
|
|
9
|
+
import { CustomElement } from '../custom-element.js';
|
|
10
|
+
import { customElementConfig } from './custom-element-config.js';
|
|
11
|
+
import { generateUniqueTagName } from '../test-setup.js';
|
|
12
|
+
import { waitForRender } from '../test-utils.js';
|
|
13
|
+
|
|
14
|
+
describe('@bind decorator', () => {
|
|
15
|
+
it('should bind method to instance', async () => {
|
|
16
|
+
const tagName = generateUniqueTagName('bind-test');
|
|
17
|
+
|
|
18
|
+
@customElementConfig({ tagName })
|
|
19
|
+
class BindTest extends CustomElement {
|
|
20
|
+
static style = ':host { display: block; }';
|
|
21
|
+
value = 'bound';
|
|
22
|
+
|
|
23
|
+
@bind
|
|
24
|
+
getValue() {
|
|
25
|
+
return this.value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const el = document.createElement(tagName) as BindTest;
|
|
34
|
+
document.body.appendChild(el);
|
|
35
|
+
await waitForRender(el);
|
|
36
|
+
|
|
37
|
+
const method = el.getValue;
|
|
38
|
+
expect(method()).toBe('bound');
|
|
39
|
+
|
|
40
|
+
document.body.removeChild(el);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should preserve this context when method is extracted', async () => {
|
|
44
|
+
const tagName = generateUniqueTagName('bind-test');
|
|
45
|
+
|
|
46
|
+
@customElementConfig({ tagName })
|
|
47
|
+
class BindTest extends CustomElement {
|
|
48
|
+
static style = ':host { display: block; }';
|
|
49
|
+
name = 'test-element';
|
|
50
|
+
|
|
51
|
+
@bind
|
|
52
|
+
getName() {
|
|
53
|
+
return this.name;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render() {
|
|
57
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const el = document.createElement(tagName) as BindTest;
|
|
62
|
+
document.body.appendChild(el);
|
|
63
|
+
await waitForRender(el);
|
|
64
|
+
|
|
65
|
+
const { getName } = el;
|
|
66
|
+
expect(getName()).toBe('test-element');
|
|
67
|
+
|
|
68
|
+
document.body.removeChild(el);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should work with callbacks', async () => {
|
|
72
|
+
const tagName = generateUniqueTagName('bind-test');
|
|
73
|
+
let result: string;
|
|
74
|
+
|
|
75
|
+
@customElementConfig({ tagName })
|
|
76
|
+
class BindTest extends CustomElement {
|
|
77
|
+
static style = ':host { display: block; }';
|
|
78
|
+
message = 'callback test';
|
|
79
|
+
|
|
80
|
+
@bind
|
|
81
|
+
handleCallback() {
|
|
82
|
+
return this.message;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render() {
|
|
86
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const el = document.createElement(tagName) as BindTest;
|
|
91
|
+
document.body.appendChild(el);
|
|
92
|
+
await waitForRender(el);
|
|
93
|
+
|
|
94
|
+
const callback = el.handleCallback;
|
|
95
|
+
result = callback();
|
|
96
|
+
|
|
97
|
+
expect(result).toBe('callback test');
|
|
98
|
+
|
|
99
|
+
document.body.removeChild(el);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle methods with arguments', async () => {
|
|
103
|
+
const tagName = generateUniqueTagName('bind-test');
|
|
104
|
+
|
|
105
|
+
@customElementConfig({ tagName })
|
|
106
|
+
class BindTest extends CustomElement {
|
|
107
|
+
static style = ':host { display: block; }';
|
|
108
|
+
|
|
109
|
+
prefixValue = 'Hello';
|
|
110
|
+
|
|
111
|
+
@bind
|
|
112
|
+
greet(name: string) {
|
|
113
|
+
return `${this.prefixValue}, ${name}!`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
render() {
|
|
117
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const el = document.createElement(tagName) as BindTest;
|
|
122
|
+
document.body.appendChild(el);
|
|
123
|
+
await waitForRender(el);
|
|
124
|
+
|
|
125
|
+
const extracted = el.greet;
|
|
126
|
+
expect(extracted('World')).toBe('Hello, World!');
|
|
127
|
+
|
|
128
|
+
document.body.removeChild(el);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should preserve bound method reference', async () => {
|
|
132
|
+
const tagName = generateUniqueTagName('bind-test');
|
|
133
|
+
|
|
134
|
+
@customElementConfig({ tagName })
|
|
135
|
+
class BindTest extends CustomElement {
|
|
136
|
+
static style = ':host { display: block; }';
|
|
137
|
+
|
|
138
|
+
valueNum = 42;
|
|
139
|
+
|
|
140
|
+
@bind
|
|
141
|
+
getValue() {
|
|
142
|
+
return this.valueNum;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
render() {
|
|
146
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const el = document.createElement(tagName) as BindTest;
|
|
151
|
+
document.body.appendChild(el);
|
|
152
|
+
await waitForRender(el);
|
|
153
|
+
|
|
154
|
+
const bound1 = el.getValue;
|
|
155
|
+
const bound2 = el.getValue;
|
|
156
|
+
|
|
157
|
+
// Should return the same bound function
|
|
158
|
+
expect(bound1).toBe(bound2);
|
|
159
|
+
expect(bound1()).toBe(42);
|
|
160
|
+
|
|
161
|
+
document.body.removeChild(el);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for @property decorator
|
|
3
|
+
* Covers type conversion, attribute reflection, custom converters, and lifecycle integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import '../test-setup.js';
|
|
8
|
+
import { property } from './property.js';
|
|
9
|
+
import { CustomElement } from '../custom-element.js';
|
|
10
|
+
import { customElementConfig } from './custom-element-config.js';
|
|
11
|
+
import { generateUniqueTagName } from '../test-setup.js';
|
|
12
|
+
import { waitForRender } from '../test-utils.js';
|
|
13
|
+
|
|
14
|
+
describe('@property decorator', () => {
|
|
15
|
+
describe('Type: String', () => {
|
|
16
|
+
it('should convert attribute to string property', async () => {
|
|
17
|
+
const tagName = generateUniqueTagName('prop-string');
|
|
18
|
+
|
|
19
|
+
@customElementConfig({ tagName })
|
|
20
|
+
class StringTest extends CustomElement {
|
|
21
|
+
static style = ':host { display: block; }';
|
|
22
|
+
@property({ type: String })
|
|
23
|
+
text = 'default';
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const el = document.createElement(tagName) as StringTest;
|
|
31
|
+
el.setAttribute('text', 'hello');
|
|
32
|
+
document.body.appendChild(el);
|
|
33
|
+
await waitForRender(el);
|
|
34
|
+
|
|
35
|
+
expect(el.text).toBe('hello');
|
|
36
|
+
|
|
37
|
+
document.body.removeChild(el);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should set property from JavaScript', async () => {
|
|
41
|
+
const tagName = generateUniqueTagName('prop-string');
|
|
42
|
+
|
|
43
|
+
@customElementConfig({ tagName })
|
|
44
|
+
class StringTest extends CustomElement {
|
|
45
|
+
static style = ':host { display: block; }';
|
|
46
|
+
@property({ type: String })
|
|
47
|
+
text = 'default';
|
|
48
|
+
|
|
49
|
+
render() {
|
|
50
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const el = document.createElement(tagName) as StringTest;
|
|
55
|
+
document.body.appendChild(el);
|
|
56
|
+
|
|
57
|
+
el.text = 'updated';
|
|
58
|
+
await waitForRender(el);
|
|
59
|
+
|
|
60
|
+
expect(el.text).toBe('updated');
|
|
61
|
+
|
|
62
|
+
document.body.removeChild(el);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Type: Number', () => {
|
|
67
|
+
it('should convert attribute to number property', async () => {
|
|
68
|
+
const tagName = generateUniqueTagName('prop-number');
|
|
69
|
+
|
|
70
|
+
@customElementConfig({ tagName })
|
|
71
|
+
class NumberTest extends CustomElement {
|
|
72
|
+
static style = ':host { display: block; }';
|
|
73
|
+
@property({ type: Number })
|
|
74
|
+
count = 0;
|
|
75
|
+
|
|
76
|
+
render() {
|
|
77
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const el = document.createElement(tagName) as NumberTest;
|
|
82
|
+
el.setAttribute('count', '42');
|
|
83
|
+
document.body.appendChild(el);
|
|
84
|
+
await waitForRender(el);
|
|
85
|
+
|
|
86
|
+
expect(el.count).toBe(42);
|
|
87
|
+
|
|
88
|
+
document.body.removeChild(el);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('Type: Boolean', () => {
|
|
93
|
+
it('should convert presence of attribute to true', async () => {
|
|
94
|
+
const tagName = generateUniqueTagName('prop-bool');
|
|
95
|
+
|
|
96
|
+
@customElementConfig({ tagName })
|
|
97
|
+
class BooleanTest extends CustomElement {
|
|
98
|
+
static style = ':host { display: block; }';
|
|
99
|
+
@property({ type: Boolean })
|
|
100
|
+
active = false;
|
|
101
|
+
|
|
102
|
+
render() {
|
|
103
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const el = document.createElement(tagName) as BooleanTest;
|
|
108
|
+
el.setAttribute('active', '');
|
|
109
|
+
document.body.appendChild(el);
|
|
110
|
+
await waitForRender(el);
|
|
111
|
+
|
|
112
|
+
expect(el.active).toBe(true);
|
|
113
|
+
|
|
114
|
+
document.body.removeChild(el);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Attribute reflection (reflect: true)', () => {
|
|
119
|
+
it('should reflect string property to attribute', async () => {
|
|
120
|
+
const tagName = generateUniqueTagName('prop-reflect');
|
|
121
|
+
|
|
122
|
+
@customElementConfig({ tagName })
|
|
123
|
+
class ReflectTest extends CustomElement {
|
|
124
|
+
static style = ':host { display: block; }';
|
|
125
|
+
@property({ type: String, reflect: true })
|
|
126
|
+
status = 'pending';
|
|
127
|
+
|
|
128
|
+
render() {
|
|
129
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const el = document.createElement(tagName) as ReflectTest;
|
|
134
|
+
document.body.appendChild(el);
|
|
135
|
+
|
|
136
|
+
el.status = 'complete';
|
|
137
|
+
await waitForRender(el);
|
|
138
|
+
|
|
139
|
+
expect(el.getAttribute('status')).toBe('complete');
|
|
140
|
+
|
|
141
|
+
document.body.removeChild(el);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should reflect boolean property to attribute', async () => {
|
|
145
|
+
const tagName = generateUniqueTagName('prop-reflect');
|
|
146
|
+
|
|
147
|
+
@customElementConfig({ tagName })
|
|
148
|
+
class ReflectTest extends CustomElement {
|
|
149
|
+
static style = ':host { display: block; }';
|
|
150
|
+
@property({ type: Boolean, reflect: true })
|
|
151
|
+
active = false;
|
|
152
|
+
|
|
153
|
+
render() {
|
|
154
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const el = document.createElement(tagName) as ReflectTest;
|
|
159
|
+
document.body.appendChild(el);
|
|
160
|
+
|
|
161
|
+
el.active = true;
|
|
162
|
+
await waitForRender(el);
|
|
163
|
+
expect(el.hasAttribute('active')).toBe(true);
|
|
164
|
+
|
|
165
|
+
el.active = false;
|
|
166
|
+
await waitForRender(el);
|
|
167
|
+
expect(el.hasAttribute('active')).toBe(false);
|
|
168
|
+
|
|
169
|
+
document.body.removeChild(el);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should reflect number property to attribute', async () => {
|
|
173
|
+
const tagName = generateUniqueTagName('prop-reflect');
|
|
174
|
+
|
|
175
|
+
@customElementConfig({ tagName })
|
|
176
|
+
class ReflectTest extends CustomElement {
|
|
177
|
+
static style = ':host { display: block; }';
|
|
178
|
+
@property({ type: Number, reflect: true })
|
|
179
|
+
count = 0;
|
|
180
|
+
|
|
181
|
+
render() {
|
|
182
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const el = document.createElement(tagName) as ReflectTest;
|
|
187
|
+
document.body.appendChild(el);
|
|
188
|
+
|
|
189
|
+
el.count = 42;
|
|
190
|
+
await waitForRender(el);
|
|
191
|
+
|
|
192
|
+
expect(el.getAttribute('count')).toBe('42');
|
|
193
|
+
|
|
194
|
+
document.body.removeChild(el);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('Property without type conversion', () => {
|
|
199
|
+
it('should handle object properties', async () => {
|
|
200
|
+
const tagName = generateUniqueTagName('prop-object');
|
|
201
|
+
|
|
202
|
+
@customElementConfig({ tagName })
|
|
203
|
+
class ObjectTest extends CustomElement {
|
|
204
|
+
static style = ':host { display: block; }';
|
|
205
|
+
@property()
|
|
206
|
+
data: any = null;
|
|
207
|
+
|
|
208
|
+
render() {
|
|
209
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const el = document.createElement(tagName) as ObjectTest;
|
|
214
|
+
document.body.appendChild(el);
|
|
215
|
+
|
|
216
|
+
const testData = { foo: 'bar', num: 123 };
|
|
217
|
+
el.data = testData;
|
|
218
|
+
await waitForRender(el);
|
|
219
|
+
|
|
220
|
+
expect(el.data).toBe(testData);
|
|
221
|
+
expect(el.data.foo).toBe('bar');
|
|
222
|
+
|
|
223
|
+
document.body.removeChild(el);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle array properties', async () => {
|
|
227
|
+
const tagName = generateUniqueTagName('prop-array');
|
|
228
|
+
|
|
229
|
+
@customElementConfig({ tagName })
|
|
230
|
+
class ArrayTest extends CustomElement {
|
|
231
|
+
static style = ':host { display: block; }';
|
|
232
|
+
@property()
|
|
233
|
+
items: string[] = [];
|
|
234
|
+
|
|
235
|
+
render() {
|
|
236
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const el = document.createElement(tagName) as ArrayTest;
|
|
241
|
+
document.body.appendChild(el);
|
|
242
|
+
|
|
243
|
+
el.items = ['a', 'b', 'c'];
|
|
244
|
+
await waitForRender(el);
|
|
245
|
+
|
|
246
|
+
expect(el.items.length).toBe(3);
|
|
247
|
+
expect(el.items[0]).toBe('a');
|
|
248
|
+
|
|
249
|
+
document.body.removeChild(el);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Custom attribute names', () => {
|
|
254
|
+
it('should support custom attribute names', async () => {
|
|
255
|
+
const tagName = generateUniqueTagName('prop-custom');
|
|
256
|
+
|
|
257
|
+
@customElementConfig({ tagName })
|
|
258
|
+
class CustomAttrTest extends CustomElement {
|
|
259
|
+
static style = ':host { display: block; }';
|
|
260
|
+
@property({ type: String, attribute: 'data-value' })
|
|
261
|
+
value = '';
|
|
262
|
+
|
|
263
|
+
render() {
|
|
264
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const el = document.createElement(tagName) as CustomAttrTest;
|
|
269
|
+
document.body.appendChild(el);
|
|
270
|
+
|
|
271
|
+
el.setAttribute('data-value', 'test');
|
|
272
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
273
|
+
|
|
274
|
+
expect(el.value).toBe('test');
|
|
275
|
+
|
|
276
|
+
document.body.removeChild(el);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|