@webtides/element-js 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -90
- package/README.md +3 -3
- package/docs/README.md +70 -89
- package/docs/concepts/classes.md +37 -66
- package/docs/concepts/hierarchy.md +29 -31
- package/docs/concepts/lifecycle.md +9 -23
- package/docs/concepts/reactivity.md +1 -3
- package/docs/features/computed-properties.md +13 -16
- package/docs/features/dependency-injection-context-protocol.md +75 -87
- package/docs/features/global-styles.md +1 -1
- package/docs/features/methods.md +14 -15
- package/docs/features/properties.md +21 -33
- package/docs/features/refs.md +23 -31
- package/docs/features/renderless.md +31 -37
- package/docs/features/store.md +63 -78
- package/docs/features/templates.md +4 -1
- package/docs/features/watch.md +28 -35
- package/docs/getting-started/upgrade-to-v1.md +16 -0
- package/docs/guide/guide.md +10 -22
- package/index.js +29 -2
- package/package.json +62 -62
- package/src/BaseElement.js +631 -631
- package/src/StyledElement.js +104 -104
- package/src/TemplateElement.js +53 -53
- package/src/dom-parts/AttributePart.js +116 -116
- package/src/dom-parts/ChildNodePart.js +288 -288
- package/src/dom-parts/NodePart.js +27 -27
- package/src/dom-parts/Part.js +6 -6
- package/src/dom-parts/TemplatePart.js +119 -119
- package/src/dom-parts/TemplateResult.js +270 -270
- package/src/dom-parts/directives.js +105 -48
- package/src/dom-parts/html.js +1 -1
- package/src/dom-parts/render.js +6 -6
- package/src/util/AttributeParser.js +47 -47
- package/src/util/DOMHelper.js +190 -190
- package/src/util/Directive.js +31 -31
- package/src/util/GlobalStylesStore.js +56 -53
- package/src/util/SerializeStateHelper.js +56 -56
- package/src/util/Store.js +174 -174
- package/src/util/defineElement.js +5 -5
- package/src/util/toString.js +5 -5
package/src/BaseElement.js
CHANGED
|
@@ -49,637 +49,637 @@ export { toString } from './util/toString.js';
|
|
|
49
49
|
* @implements {Serializable}
|
|
50
50
|
*/
|
|
51
51
|
class BaseElement extends HTMLElement {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
52
|
+
_serializationKey;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {BaseElementOptions} options
|
|
56
|
+
*/
|
|
57
|
+
constructor(options = {}) {
|
|
58
|
+
super();
|
|
59
|
+
this.$refs = {};
|
|
60
|
+
this._state = {};
|
|
61
|
+
this._mutationObserver = null;
|
|
62
|
+
this._registeredEvents = [];
|
|
63
|
+
this._batchUpdate = null;
|
|
64
|
+
this._requestedUpdates = [];
|
|
65
|
+
this._options = {
|
|
66
|
+
autoUpdate: true,
|
|
67
|
+
deferUpdate: true,
|
|
68
|
+
deferConnected: false,
|
|
69
|
+
mutationObserverOptions: {
|
|
70
|
+
attributes: true,
|
|
71
|
+
childList: true,
|
|
72
|
+
subtree: false,
|
|
73
|
+
...options.mutationObserverOptions,
|
|
74
|
+
},
|
|
75
|
+
propertyOptions: {},
|
|
76
|
+
...options,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Overrides the native `connectedCallback` of the HTMLElement to set up and initialize our element.
|
|
82
|
+
* This will define the attributes and properties as reactive getters and setter,
|
|
83
|
+
* register observers, refs and events.
|
|
84
|
+
*/
|
|
85
|
+
connectedCallback() {
|
|
86
|
+
if (this.hasAttribute('defer-update')) {
|
|
87
|
+
this._options.deferUpdate = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.hasAttribute('defer-connected') || this._options.deferConnected) {
|
|
91
|
+
this._options.deferConnected = false;
|
|
92
|
+
this.removeAttribute('defer-connected');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.hasAttribute('ejs:key')) {
|
|
97
|
+
this._serializationKey = this.getAttribute('ejs:key');
|
|
98
|
+
deserializeState(this);
|
|
99
|
+
} else {
|
|
100
|
+
this._serializationKey = globalThis.crypto.randomUUID();
|
|
101
|
+
if (globalThis.elementJsConfig?.serializeState) {
|
|
102
|
+
this.setAttribute('ejs:key', this._serializationKey);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// define all attributes to "this" as properties
|
|
107
|
+
this.defineAttributesAsProperties();
|
|
108
|
+
|
|
109
|
+
// define all properties to "this"
|
|
110
|
+
this.defineProperties();
|
|
111
|
+
|
|
112
|
+
// define context
|
|
113
|
+
this.definePropertyInjection();
|
|
114
|
+
|
|
115
|
+
// define everything that should be observed
|
|
116
|
+
this.defineObserver();
|
|
117
|
+
|
|
118
|
+
if (this._options.deferUpdate) {
|
|
119
|
+
// don't update/render, but register refs and events
|
|
120
|
+
this.registerEventsAndRefs();
|
|
121
|
+
|
|
122
|
+
this.triggerHook('connected');
|
|
123
|
+
} else {
|
|
124
|
+
this.requestUpdate({ notify: false }).then(() => {
|
|
125
|
+
this.triggerHook('connected');
|
|
126
|
+
this.triggerHook('afterUpdate');
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Define a MutationObserver on the element to get notified on attribute changes and childList updates according to the options specified via the constructor options.
|
|
133
|
+
*/
|
|
134
|
+
defineObserver() {
|
|
135
|
+
// see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#Example_usage
|
|
136
|
+
this._mutationObserver = new MutationObserver((mutations) => {
|
|
137
|
+
mutations.forEach((mutation) => {
|
|
138
|
+
if (
|
|
139
|
+
mutation.type === 'attributes' &&
|
|
140
|
+
mutation.target === this &&
|
|
141
|
+
this._state.hasOwnProperty(dashToCamel(mutation.attributeName))
|
|
142
|
+
) {
|
|
143
|
+
// update property by invoking the setter
|
|
144
|
+
this[dashToCamel(mutation.attributeName)] = parseAttribute(
|
|
145
|
+
this.getAttribute(mutation.attributeName),
|
|
146
|
+
this._options.propertyOptions[dashToCamel(mutation.attributeName)],
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (mutation.type === 'attributes' && mutation.target !== this) {
|
|
150
|
+
this.requestUpdate();
|
|
151
|
+
}
|
|
152
|
+
if (mutation.type === 'childList') {
|
|
153
|
+
this.requestUpdate();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this._mutationObserver.observe(this, {
|
|
159
|
+
attributes: true,
|
|
160
|
+
childList: true,
|
|
161
|
+
subtree: false,
|
|
162
|
+
...this._options.mutationObserverOptions,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Native `disconnectedCallback` of the HTMLElement
|
|
168
|
+
*/
|
|
169
|
+
disconnectedCallback() {
|
|
170
|
+
// remove events for elements in the component
|
|
171
|
+
this.removeEvents();
|
|
172
|
+
|
|
173
|
+
if (Object.keys(this.provideProperties()).length > 0) {
|
|
174
|
+
this.removeEventListener('request-context', this.onRequestContext);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// remove observers
|
|
178
|
+
if (this._mutationObserver) this._mutationObserver.disconnect();
|
|
179
|
+
|
|
180
|
+
// unsubscribe from stores
|
|
181
|
+
Object.values(this.properties()).forEach((property) => {
|
|
182
|
+
if (property instanceof Store) {
|
|
183
|
+
property.unsubscribe(this);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
this.triggerHook('disconnected');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Request and batch an asynchronous update cycle
|
|
192
|
+
* @param {PropertyUpdateOptions} options
|
|
193
|
+
* @returns {Promise<unknown>}
|
|
194
|
+
*/
|
|
195
|
+
requestUpdate(options = { notify: true }) {
|
|
196
|
+
if (options.notify === true) {
|
|
197
|
+
this.triggerHook('beforeUpdate');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this._batchUpdate) {
|
|
201
|
+
cancelAnimationFrame(this._batchUpdate);
|
|
202
|
+
this._batchUpdate = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
this._requestedUpdates.push({ resolve, reject });
|
|
207
|
+
this._batchUpdate = requestAnimationFrame(() => {
|
|
208
|
+
try {
|
|
209
|
+
this.update(options);
|
|
210
|
+
this._requestedUpdates.forEach(({ resolve, reject }) => resolve());
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// console.error('Update error', e)
|
|
213
|
+
this._requestedUpdates.forEach(({ resolve, reject }) => reject(e));
|
|
214
|
+
}
|
|
215
|
+
this._requestedUpdates = [];
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* This should be called by the template component AFTER adding the template to
|
|
222
|
+
* the DOM.
|
|
223
|
+
* Here we will register the events and the refs for the element.
|
|
224
|
+
* @param {PropertyUpdateOptions} options
|
|
225
|
+
*/
|
|
226
|
+
update(options = { notify: true }) {
|
|
227
|
+
this.registerEventsAndRefs();
|
|
228
|
+
if (options.notify === true) {
|
|
229
|
+
this.triggerHook('afterUpdate');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Register events and refs for the component
|
|
235
|
+
*/
|
|
236
|
+
registerEventsAndRefs() {
|
|
237
|
+
// register events for elements in the component
|
|
238
|
+
this.registerEvents();
|
|
239
|
+
|
|
240
|
+
// register $refs
|
|
241
|
+
this.registerRefs();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Defines all attributes assigned in the HTML for the element as properties on the element
|
|
246
|
+
* If the value for an attribute can be parsed to an object or array, it will do so
|
|
247
|
+
*/
|
|
248
|
+
defineAttributesAsProperties() {
|
|
249
|
+
const ignoreAttributes = ['class', 'style'];
|
|
250
|
+
Array.from(this.attributes)
|
|
251
|
+
.filter((attribute) => {
|
|
252
|
+
return !ignoreAttributes.includes(attribute.name);
|
|
253
|
+
})
|
|
254
|
+
.forEach((attribute) => {
|
|
255
|
+
const name = dashToCamel(attribute.name);
|
|
256
|
+
const parseOptions = this._options.propertyOptions[name];
|
|
257
|
+
this.defineProperty(name, parseAttribute(attribute.value, parseOptions), true);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Properties to be defined and assigned on the element. Should be overwritten in child components
|
|
263
|
+
* Properties defined here will also trigger a update() when changed.
|
|
264
|
+
* eg. { stringProperty: 'value', numberProperty: 13, booleanProperty: true } }
|
|
265
|
+
* @return {{}}
|
|
266
|
+
*/
|
|
267
|
+
properties() {
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Defines properties on the element based on keys from this.properties()
|
|
273
|
+
* Will trigger update() when a property was changed
|
|
274
|
+
*/
|
|
275
|
+
defineProperties() {
|
|
276
|
+
Object.keys({ ...this.injectProperties(), ...this.properties() }).forEach((prop) => {
|
|
277
|
+
// when mixing shadow dom elements with light dom elements and nesting custom elements
|
|
278
|
+
// it might occur that properties where set on an element before it was
|
|
279
|
+
// registered or connected. To avoid such timing issues we check
|
|
280
|
+
// if a value was set for that specific property on the
|
|
281
|
+
// prototype before assigning a default value
|
|
282
|
+
const value = this[prop] || this.properties()[prop];
|
|
283
|
+
this.defineProperty(prop, value);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Define a property on the element
|
|
289
|
+
* Will trigger watch() function when a property was changed
|
|
290
|
+
* Will trigger update() when a property was changed
|
|
291
|
+
* @param {string} property
|
|
292
|
+
* @param {any} value
|
|
293
|
+
* @param {boolean} reflectAttribute
|
|
294
|
+
*/
|
|
295
|
+
defineProperty(property, value, reflectAttribute = false) {
|
|
296
|
+
if (this._state.hasOwnProperty(property)) {
|
|
297
|
+
// property has already been defined as an attribute nothing to do here
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// if property did not come from an attribute but has the option to reflect // enabled or custom fn
|
|
302
|
+
if (!reflectAttribute && this._options.propertyOptions[property]?.reflect) {
|
|
303
|
+
this.reflectProperty({ property: property, newValue: value });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// remove attribute if reflect is set to false explicitly in options
|
|
307
|
+
if (this._options.propertyOptions[property]?.reflect === false) {
|
|
308
|
+
this.removeAttribute(camelToDash(property));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (hasSerializedState(this._serializationKey) && Object.keys(this.serializeState())?.includes(property)) {
|
|
312
|
+
value = this[property];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this._state[property] = value;
|
|
316
|
+
|
|
317
|
+
if (this._state[property] instanceof Store) {
|
|
318
|
+
this._state[property].subscribe(this);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
Object.defineProperty(this, property, {
|
|
322
|
+
get: () => {
|
|
323
|
+
return this._state[property];
|
|
324
|
+
},
|
|
325
|
+
set: (newValue) => {
|
|
326
|
+
const oldValue = this._state[property];
|
|
327
|
+
const newValueString = JSON.stringify(newValue);
|
|
328
|
+
if (JSON.stringify(oldValue) !== newValueString) {
|
|
329
|
+
this._state[property] = newValue;
|
|
330
|
+
|
|
331
|
+
serializeState(this);
|
|
332
|
+
|
|
333
|
+
if (newValue instanceof Store) {
|
|
334
|
+
newValue.subscribe(this);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (reflectAttribute || this._options.propertyOptions[property]?.reflect) {
|
|
338
|
+
this.reflectProperty({ property, newValue, newValueString });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (this._options.autoUpdate) {
|
|
342
|
+
this.requestUpdate({
|
|
343
|
+
notify: true,
|
|
344
|
+
property: property,
|
|
345
|
+
newValue: newValue,
|
|
346
|
+
newValueString: newValueString,
|
|
347
|
+
oldValue: oldValue,
|
|
348
|
+
}).finally(() => {
|
|
349
|
+
this.callPropertyWatcher(property, newValue, oldValue);
|
|
350
|
+
this.notifyPropertyChange(property, newValue);
|
|
351
|
+
});
|
|
352
|
+
} else {
|
|
353
|
+
this.callPropertyWatcher(property, newValue, oldValue);
|
|
354
|
+
this.notifyPropertyChange(property, newValue);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return this;
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Call the watch callbacks on property changes
|
|
364
|
+
* @param {string} property property that changes
|
|
365
|
+
* @param {any} newValue
|
|
366
|
+
* @param {any} oldValue
|
|
367
|
+
*/
|
|
368
|
+
callPropertyWatcher(property, newValue, oldValue) {
|
|
369
|
+
// notify watched properties (after update())
|
|
370
|
+
const watch = this.watch();
|
|
371
|
+
if (property in watch) {
|
|
372
|
+
watch[property](newValue, oldValue);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Notify property observer via change event
|
|
378
|
+
* @param {string} property property that changes
|
|
379
|
+
* @param {any} newValue
|
|
380
|
+
*/
|
|
381
|
+
notifyPropertyChange(property, newValue) {
|
|
382
|
+
// dispatch change event
|
|
383
|
+
if (
|
|
384
|
+
property in this._options['propertyOptions'] &&
|
|
385
|
+
this._options['propertyOptions'][property]['notify'] === true
|
|
386
|
+
) {
|
|
387
|
+
this.dispatch(`${camelToDash(property)}-changed`, newValue);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Reflects a property as attribute on the element
|
|
393
|
+
* @param {PropertyUpdateOptions} options
|
|
394
|
+
*/
|
|
395
|
+
reflectProperty(options) {
|
|
396
|
+
const { property, newValue } = options;
|
|
397
|
+
const newValueString = options.newValueString || JSON.stringify(newValue);
|
|
398
|
+
|
|
399
|
+
if (newValue === undefined || newValue === null || isNaN(newValue)) {
|
|
400
|
+
// these would be reflected as strings: "undefined" || "null" || "NaN"
|
|
401
|
+
// which is not the desired behaviour. Therefore we reflect them as empty strings
|
|
402
|
+
this.setAttribute(camelToDash(property), '');
|
|
403
|
+
} else {
|
|
404
|
+
let attributeValue = isObjectLike(newValue) ? newValueString : newValue;
|
|
405
|
+
if (typeof this._options.propertyOptions[property]?.reflect === 'function') {
|
|
406
|
+
attributeValue = this._options.propertyOptions[property].reflect(newValue);
|
|
407
|
+
}
|
|
408
|
+
this.setAttribute(camelToDash(property), attributeValue);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Context Properties to issue Context Requests and to pull contextual properties into the elements scope
|
|
414
|
+
* @return {{}}
|
|
415
|
+
*/
|
|
416
|
+
injectProperties() {
|
|
417
|
+
return {};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Define which properties that can be provided to children
|
|
422
|
+
* @return {{}}
|
|
423
|
+
*/
|
|
424
|
+
provideProperties() {
|
|
425
|
+
return {};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Defines context on the element based on keys from this.injectProperties()
|
|
430
|
+
*/
|
|
431
|
+
definePropertyInjection() {
|
|
432
|
+
Object.entries(this.injectProperties()).forEach(([key, value]) => {
|
|
433
|
+
this.requestContext(key, value);
|
|
434
|
+
});
|
|
435
|
+
// define context provider
|
|
436
|
+
const providedKeys = Object.keys(this.provideProperties());
|
|
437
|
+
if (providedKeys.length > 0) {
|
|
438
|
+
this.addEventListener('request-context', this.onRequestContext);
|
|
439
|
+
// check if there are already connected elements in child dom and restart requests
|
|
440
|
+
getAllElementChildren(this.getRoot(), BaseElement).forEach((customChild) => {
|
|
441
|
+
// if injectProperties?.() is defined it means that the child got connected BEFORE the parent (Runtime Issue)
|
|
442
|
+
const requestedProperties = customChild.injectProperties?.() ?? {};
|
|
443
|
+
Object.entries(requestedProperties).forEach(([key, value]) => {
|
|
444
|
+
// iterate over requested properties to find missed context requests
|
|
445
|
+
if (providedKeys.includes(key)) {
|
|
446
|
+
// owns the provided key -> replay request
|
|
447
|
+
customChild.requestContext(key, value);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Helper Function to initialize the context requests
|
|
456
|
+
* @param {string} propertyName
|
|
457
|
+
* @param {any | function} valueOrCallback
|
|
458
|
+
*/
|
|
459
|
+
requestContext(propertyName, valueOrCallback) {
|
|
460
|
+
this.dispatch('request-context', { [propertyName]: valueOrCallback });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Internal Context Request Handler
|
|
465
|
+
* This will check if the current/receiving instance actually provides a value under the requested name.
|
|
466
|
+
* If there is something to provide it will either assign the value to the requesting element or call the callback.
|
|
467
|
+
* @param {CustomEvent} event
|
|
468
|
+
*/
|
|
469
|
+
onRequestContext(event) {
|
|
470
|
+
const provideProperties = this.provideProperties();
|
|
471
|
+
|
|
472
|
+
Object.entries(event.detail ?? {}).forEach(([key, valueOrCallback]) => {
|
|
473
|
+
if (key in provideProperties) {
|
|
474
|
+
// found it, provide it
|
|
475
|
+
const providedValue = provideProperties[key];
|
|
476
|
+
event.stopPropagation();
|
|
477
|
+
if (typeof valueOrCallback === 'function') {
|
|
478
|
+
// call function with context value
|
|
479
|
+
valueOrCallback(providedValue);
|
|
480
|
+
} else {
|
|
481
|
+
// assign to prop
|
|
482
|
+
const eventPath = event.composedPath();
|
|
483
|
+
const target = eventPath[0] || event.target;
|
|
484
|
+
target[key] = providedValue;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Connected lifecycle hook
|
|
492
|
+
*/
|
|
493
|
+
connected() {}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* BeforeUpdate lifecycle hook
|
|
497
|
+
*/
|
|
498
|
+
beforeUpdate() {}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* AfterUpdate lifecycle hook
|
|
502
|
+
*/
|
|
503
|
+
afterUpdate() {}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Disconnected lifecycle hook
|
|
507
|
+
*/
|
|
508
|
+
disconnected() {}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Triggers a lifecycle hook based on the name
|
|
512
|
+
* @param {string} name
|
|
513
|
+
*/
|
|
514
|
+
triggerHook(name) {
|
|
515
|
+
if (name in this) {
|
|
516
|
+
this[name]();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Watched attributes & properties to be notified to the element when changed. Should be overwritten in child components
|
|
522
|
+
* When an attribute or property as the key was changed on the element, the callback function defined as value
|
|
523
|
+
* will be called with `newValue` and `oldValue`
|
|
524
|
+
* eg. { property: (oldValue, newValue) => { console.log('property changed!, oldValue, newValue); } }
|
|
525
|
+
* @return {{}}
|
|
526
|
+
*/
|
|
527
|
+
watch() {
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Return a map of events that should be registered on the element.
|
|
533
|
+
* eg. { '.link': {
|
|
534
|
+
* 'click': (e) => {console.log('MyElement.link - clicked', e)},
|
|
535
|
+
* 'mouseover': (e) => {console.log('MyElement.link - mouseover()', e)},
|
|
536
|
+
* }
|
|
537
|
+
* }
|
|
538
|
+
*/
|
|
539
|
+
events() {
|
|
540
|
+
return {};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Registers events on the element based on events().
|
|
545
|
+
* Removes all previously registered events before adding them.
|
|
546
|
+
*/
|
|
547
|
+
registerEvents() {
|
|
548
|
+
// remove all previously registered events to prevent duplicate triggers
|
|
549
|
+
this.removeEvents();
|
|
550
|
+
|
|
551
|
+
// register events
|
|
552
|
+
const eventDefinitions = this.events();
|
|
553
|
+
Object.keys(eventDefinitions).forEach((elementSelector) => {
|
|
554
|
+
const events = eventDefinitions[elementSelector];
|
|
555
|
+
const selectorWhiteList = { window, document, this: this.getRoot() };
|
|
556
|
+
const whiteListElement = selectorWhiteList[elementSelector];
|
|
557
|
+
const eventTargets = whiteListElement
|
|
558
|
+
? [whiteListElement]
|
|
559
|
+
: this.getRoot().querySelectorAll(elementSelector);
|
|
560
|
+
|
|
561
|
+
eventTargets.forEach((eventTarget) => {
|
|
562
|
+
Object.keys(events).forEach((eventName) => {
|
|
563
|
+
const notation = events[eventName];
|
|
564
|
+
if (typeof notation === 'function') {
|
|
565
|
+
const callback = notation.bind(this);
|
|
566
|
+
eventTarget.addEventListener(eventName, callback);
|
|
567
|
+
this._registeredEvents.push({ eventTarget, eventName, callback });
|
|
568
|
+
} else if (typeof notation === 'object' && typeof notation.listener === 'function') {
|
|
569
|
+
const callback = notation.listener.bind(this);
|
|
570
|
+
eventTarget.addEventListener(eventName, callback, notation.options ?? {});
|
|
571
|
+
this._registeredEvents.push({ eventTarget, eventName, callback });
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Removes all events from the element that where previously registered from the events() map.
|
|
580
|
+
*/
|
|
581
|
+
removeEvents() {
|
|
582
|
+
this._registeredEvents.forEach(({ eventTarget, eventName, callback }) => {
|
|
583
|
+
if (eventTarget === window || eventTarget === document || this.getRoot().contains(eventTarget)) {
|
|
584
|
+
eventTarget.removeEventListener(eventName, callback);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
this._registeredEvents = [];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Stores an object that references child elements & DOM nodes that have a ref attribute defined.
|
|
592
|
+
* References will be accessible on the element under this.$refs
|
|
593
|
+
*/
|
|
594
|
+
registerRefs() {
|
|
595
|
+
const refsNodeList = this.getRoot().querySelectorAll('[ref]');
|
|
596
|
+
const refsArray = Array.from(refsNodeList);
|
|
597
|
+
const refsMap = {};
|
|
598
|
+
|
|
599
|
+
refsArray.forEach((refNode) => {
|
|
600
|
+
const refKey = refNode.getAttribute('ref');
|
|
601
|
+
const isListKey = refKey.endsWith('[]');
|
|
602
|
+
const cleanKey = isListKey ? refKey.slice(0, -2) : refKey;
|
|
603
|
+
const hasListRef = Array.isArray(refsMap[cleanKey]);
|
|
604
|
+
|
|
605
|
+
if (isListKey) {
|
|
606
|
+
if (!refsMap[cleanKey] || !hasListRef) {
|
|
607
|
+
// create or overwrite previously registered single ref
|
|
608
|
+
refsMap[cleanKey] = [];
|
|
609
|
+
}
|
|
610
|
+
refsMap[cleanKey].push(refNode);
|
|
611
|
+
} else if (!hasListRef) {
|
|
612
|
+
refsMap[refKey] = refNode;
|
|
613
|
+
} else {
|
|
614
|
+
console.warn(
|
|
615
|
+
`Did not register singular ref ${refKey}, ${refNode} as a ref list is already registered under the same name.`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
this.$refs = refsMap;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Helper function for dispatching custom events
|
|
625
|
+
* @param {string} name
|
|
626
|
+
* @param {Object} [data]
|
|
627
|
+
* @param {CustomEventInit} [options]
|
|
628
|
+
* @param {boolean} [cancelable] - DEPRECATED
|
|
629
|
+
* @param {boolean} [composed] - DEPRECATED
|
|
630
|
+
*/
|
|
631
|
+
dispatch(name, data, options, cancelable = false, composed = false) {
|
|
632
|
+
let eventInit = {};
|
|
633
|
+
|
|
634
|
+
if (options !== undefined && typeof options === 'boolean') {
|
|
635
|
+
eventInit['bubbles'] = options;
|
|
636
|
+
eventInit['cancelable'] = cancelable;
|
|
637
|
+
eventInit['composed'] = composed;
|
|
638
|
+
eventInit['detail'] = data;
|
|
639
|
+
console.warn(
|
|
640
|
+
`[${this.localName}] Using the old parameter options for the event is deprecated and will be removed in v2.0! Please use the new options object that is in line with the "CustomEventInit" options`,
|
|
641
|
+
);
|
|
642
|
+
} else {
|
|
643
|
+
eventInit = {
|
|
644
|
+
bubbles: true,
|
|
645
|
+
cancelable: true,
|
|
646
|
+
composed: true,
|
|
647
|
+
detail: data,
|
|
648
|
+
...options,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const event = new CustomEvent(name, eventInit);
|
|
653
|
+
this.dispatchEvent(event);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Get the root element
|
|
658
|
+
* @returns {HTMLElement}
|
|
659
|
+
*/
|
|
660
|
+
getRoot() {
|
|
661
|
+
return this;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* This method will be used to serialize this elements state.
|
|
666
|
+
* By default, it will generate an object with all properties from this.properties().
|
|
667
|
+
* You can override this and return a different object with more/less/different properties.
|
|
668
|
+
* @return {{[string: any]: *}}
|
|
669
|
+
*/
|
|
670
|
+
serializeState() {
|
|
671
|
+
const keys = Object.keys(this.properties());
|
|
672
|
+
return Object.fromEntries(keys.map((key) => [key, this[key]]));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* This method will be used to restore properties on the element from a previously serialized state.
|
|
677
|
+
* You can override this and handle the deserialization differently if the default is not enough.
|
|
678
|
+
* @param {{[string: any]: *}} serializedState
|
|
679
|
+
*/
|
|
680
|
+
restoreState(serializedState) {
|
|
681
|
+
Object.assign(this, serializedState);
|
|
682
|
+
}
|
|
683
683
|
}
|
|
684
684
|
|
|
685
685
|
export { BaseElement, getClosestParentCustomElementNode, isOfSameNodeType };
|