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
@@ -125,6 +125,20 @@ export abstract class CustomElement extends HTMLElement {
125
125
  return this.#connected;
126
126
  }
127
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
+
128
142
  /**
129
143
  * Promise that resolves when the component has finished updating
130
144
  * and rendering to the DOM.
@@ -403,7 +417,9 @@ export abstract class CustomElement extends HTMLElement {
403
417
  if (typeof (this as any).init === "function") {
404
418
  (this as any).init();
405
419
  this.#controllers.forEach((controller) => {
406
- controller?.init();
420
+ if (controller?.init) {
421
+ controller.init();
422
+ }
407
423
  });
408
424
  }
409
425
  }
@@ -28,4 +28,7 @@ customElements.whenDefined("custom-style").then(() => {
28
28
  document.body.parentElement.classList.add("custom-style-defined");
29
29
  });
30
30
 
31
- customElements.define("custom-style", CustomStyleElement, { extends: "link" });
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
+ });
@@ -1,46 +1,46 @@
1
- export const bind = (target, key, descriptor) => {
2
- let fn = descriptor.value;
3
- // console.warn("@Bind decorator is deprecated, use arrow function expression");
4
- if (typeof fn !== "function") {
5
- throw new Error(
6
- `@Bind decorator can only be applied to methods not: ${typeof fn}`
7
- );
8
- }
9
-
10
- // In IE11 calling Object.defineProperty has a side-effect of evaluating the
11
- // getter for the property which is being replaced. This causes infinite
12
- // recursion and an "Out of stack space" error.
13
- let definingProperty = false;
14
-
15
- return {
16
- configurable: true,
17
- get() {
18
- if (
19
- definingProperty ||
20
- this === target.prototype ||
21
- this.hasOwnProperty(key) ||
22
- typeof fn !== "function"
23
- ) {
24
- return fn;
25
- }
26
-
27
- let boundFn = fn.bind(this);
28
- definingProperty = true;
29
- Object.defineProperty(this, key, {
30
- configurable: true,
31
- get() {
32
- return boundFn;
33
- },
34
- set(value) {
35
- fn = value;
36
- delete this[key];
37
- },
38
- });
39
- definingProperty = false;
40
- return boundFn;
41
- },
42
- set(value) {
43
- fn = value;
44
- },
45
- };
46
- };
1
+ export const bind = (target, key, descriptor) => {
2
+ let fn = descriptor.value;
3
+ // console.warn("@Bind decorator is deprecated, use arrow function expression");
4
+ if (typeof fn !== "function") {
5
+ throw new Error(
6
+ `@Bind decorator can only be applied to methods not: ${typeof fn}`
7
+ );
8
+ }
9
+
10
+ // In IE11 calling Object.defineProperty has a side-effect of evaluating the
11
+ // getter for the property which is being replaced. This causes infinite
12
+ // recursion and an "Out of stack space" error.
13
+ let definingProperty = false;
14
+
15
+ return {
16
+ configurable: true,
17
+ get() {
18
+ if (
19
+ definingProperty ||
20
+ this === target.prototype ||
21
+ this.hasOwnProperty(key) ||
22
+ typeof fn !== "function"
23
+ ) {
24
+ return fn;
25
+ }
26
+
27
+ let boundFn = fn.bind(this);
28
+ definingProperty = true;
29
+ Object.defineProperty(this, key, {
30
+ configurable: true,
31
+ get() {
32
+ return boundFn;
33
+ },
34
+ set(value) {
35
+ fn = value;
36
+ delete this[key];
37
+ },
38
+ });
39
+ definingProperty = false;
40
+ return boundFn;
41
+ },
42
+ set(value) {
43
+ fn = value;
44
+ },
45
+ };
46
+ };
@@ -1,17 +1,17 @@
1
- export interface IElementConfig {
2
- tagName: string;
3
- options?: {
4
- extends: string;
5
- };
6
- }
7
- export const customElementConfig = (config: IElementConfig) => {
8
- return (Element) => {
9
- if (customElements.get(config.tagName)) {
10
- console.warn(
11
- `Custom element with tag name ${config.tagName} already exists.`
12
- );
13
- return;
14
- }
15
- customElements.define(config.tagName, Element, config.options);
16
- };
17
- };
1
+ export interface IElementConfig {
2
+ tagName: string;
3
+ options?: {
4
+ extends: string;
5
+ };
6
+ }
7
+ export const customElementConfig = (config: IElementConfig) => {
8
+ return (Element) => {
9
+ if (customElements.get(config.tagName)) {
10
+ console.warn(
11
+ `Custom element with tag name ${config.tagName} already exists.`
12
+ );
13
+ return;
14
+ }
15
+ customElements.define(config.tagName, Element, config.options);
16
+ };
17
+ };
@@ -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
+ });