@webtides/element-js 1.0.0-alpha.1 → 1.0.0-alpha.11
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/docs/README.md +174 -0
- package/docs/concepts/classes.md +167 -0
- package/docs/concepts/hierarchy.md +61 -0
- package/docs/concepts/lifecycle.md +45 -0
- package/docs/concepts/reactivity.md +14 -0
- package/docs/features/computed-properties.md +28 -0
- package/docs/features/dependency-injection-context-protocol.md +132 -0
- package/docs/features/events.md +155 -0
- package/docs/features/global-styles.md +131 -0
- package/docs/features/methods.md +34 -0
- package/docs/features/properties.md +56 -0
- package/docs/features/refs.md +47 -0
- package/docs/features/renderless.md +70 -0
- package/docs/features/store.md +143 -0
- package/docs/features/styles.md +169 -0
- package/docs/features/templates.md +308 -0
- package/docs/features/watch.md +54 -0
- package/docs/getting-started/upgrade-to-v1.md +243 -0
- package/docs/guide/guide.md +112 -0
- package/index.js +9 -2
- package/package.json +15 -19
- package/src/BaseElement.js +206 -42
- package/src/StyledElement.js +67 -48
- package/src/TemplateElement.js +32 -7
- package/src/dom-parts/AttributePart.js +16 -9
- package/src/dom-parts/ChildNodePart.js +8 -22
- package/src/dom-parts/NodePart.js +1 -0
- package/src/dom-parts/TemplatePart.js +19 -25
- package/src/dom-parts/TemplateResult.js +46 -35
- package/src/dom-parts/directives.js +43 -16
- package/src/dom-parts/html.js +1 -1
- package/src/dom-parts/render.js +6 -1
- package/src/util/AttributeParser.js +54 -21
- package/src/util/DOMHelper.js +63 -14
- package/src/util/Directive.js +21 -1
- package/src/util/GlobalStylesStore.js +66 -0
- package/src/util/SerializeStateHelper.js +103 -0
- package/src/util/Store.js +111 -15
- package/src/util/defineElement.js +2 -2
- package/src/util/toString.js +2 -2
- package/dist/index.js +0 -1330
- package/dist/index.js.map +0 -7
- package/src/util/i18n.js +0 -20
package/src/BaseElement.js
CHANGED
|
@@ -1,11 +1,59 @@
|
|
|
1
1
|
import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from './util/AttributeParser.js';
|
|
2
|
-
import { getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
|
|
2
|
+
import { getAllElementChildren, getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
|
|
3
3
|
import { Store } from './util/Store.js';
|
|
4
|
-
|
|
4
|
+
import { serializeState, deserializeState, hasSerializedState } from './util/SerializeStateHelper.js';
|
|
5
5
|
export { defineElement } from './util/defineElement.js';
|
|
6
6
|
export { toString } from './util/toString.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Options object for the BaseElement
|
|
10
|
+
* @typedef {Object} BaseElementOptions
|
|
11
|
+
* @property {boolean} [autoUpdate] - When set to true the element will call the requestUpdate() method on the instance every time a property or attribute was changed. This will re-evaluate everything on the element and trigger a re-render (if a template is provided) and trigger the watchers for the affected properties/attributes. Default is `true`
|
|
12
|
+
* @property {boolean} [deferUpdate] - When set to true the element will not call the requestUpdate() method upon connecting and therefore will not render (if template was provided) initially. Default is `true`
|
|
13
|
+
* @property {boolean} [deferConnected] - When set to true the element will skip the initialization in connectedCallback() before connecting and therefore will not render (if template was provided) or hydrate initially. Default is `false`
|
|
14
|
+
* @property {MutationObserverOptions} [mutationObserverOptions]
|
|
15
|
+
* @property {PropertyOptions} [propertyOptions]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options object for the BaseElement mutationObserverOptions option
|
|
20
|
+
* @typedef {Object} MutationObserverOptions
|
|
21
|
+
* @property {boolean} [childList] - When childList is set to true the element will also call the requestUpdate method for childList modifications (eg. Adding or removing child elements). Default is `true`
|
|
22
|
+
* @property {boolean} [subtree] - Please note that observing subtree mutations might have performance implications and use it only if necessary. Default is `false`
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options object for the BaseElement propertyOptions option
|
|
27
|
+
* @typedef {Object<string, PropertyOption>} PropertyOptions
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Option object for a property of the BaseElement propertyOptions option
|
|
32
|
+
* @typedef {Object} PropertyOption
|
|
33
|
+
* @property {undefined | boolean | function} [reflect] - When set to true the element will reflect property changes back to attributes if the attribute was not present when connecting the element. By default, all attributes that are present when connecting the element will be reflected anyway. Default is `undefined`
|
|
34
|
+
* @property {undefined | boolean | function} [parse] - When set to false the element will not automatically try to parse the attributes string value to a complex type (number, array, object). Default is `undefined`
|
|
35
|
+
* @property {undefined | boolean | function} [notify] - When set to true the element will dispatch CustomEvents for every property/attribute change. The event name will be the property name all lowercase and camel to dashes with a postfix of -changed. Default is `undefined`
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Options object for updating properties
|
|
40
|
+
* @typedef {Object} PropertyUpdateOptions
|
|
41
|
+
* @property {boolean} [notify] - Whether the update should trigger the lifecycle methods. The default is `true`
|
|
42
|
+
* @property {string} property - The property name
|
|
43
|
+
* @property {any} newValue - The new value for the property
|
|
44
|
+
* @property {string} newValueString - The new value as `string` for the property (for comparison reasons)
|
|
45
|
+
* @property {any} oldValue - The old value for the property
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @implements {Serializable}
|
|
50
|
+
*/
|
|
8
51
|
class BaseElement extends HTMLElement {
|
|
52
|
+
_serializationKey;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {BaseElementOptions} options
|
|
56
|
+
*/
|
|
9
57
|
constructor(options = {}) {
|
|
10
58
|
super();
|
|
11
59
|
this.$refs = {};
|
|
@@ -17,6 +65,7 @@ class BaseElement extends HTMLElement {
|
|
|
17
65
|
this._options = {
|
|
18
66
|
autoUpdate: true,
|
|
19
67
|
deferUpdate: true,
|
|
68
|
+
deferConnected: false,
|
|
20
69
|
mutationObserverOptions: {
|
|
21
70
|
attributes: true,
|
|
22
71
|
childList: true,
|
|
@@ -28,7 +77,32 @@ class BaseElement extends HTMLElement {
|
|
|
28
77
|
};
|
|
29
78
|
}
|
|
30
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
|
+
*/
|
|
31
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
|
+
|
|
32
106
|
// define all attributes to "this" as properties
|
|
33
107
|
this.defineAttributesAsProperties();
|
|
34
108
|
|
|
@@ -41,7 +115,7 @@ class BaseElement extends HTMLElement {
|
|
|
41
115
|
// define everything that should be observed
|
|
42
116
|
this.defineObserver();
|
|
43
117
|
|
|
44
|
-
if (this.
|
|
118
|
+
if (this._options.deferUpdate) {
|
|
45
119
|
// don't update/render, but register refs and events
|
|
46
120
|
this.registerEventsAndRefs();
|
|
47
121
|
|
|
@@ -54,6 +128,9 @@ class BaseElement extends HTMLElement {
|
|
|
54
128
|
}
|
|
55
129
|
}
|
|
56
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
|
+
*/
|
|
57
134
|
defineObserver() {
|
|
58
135
|
// see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#Example_usage
|
|
59
136
|
this._mutationObserver = new MutationObserver((mutations) => {
|
|
@@ -86,6 +163,9 @@ class BaseElement extends HTMLElement {
|
|
|
86
163
|
});
|
|
87
164
|
}
|
|
88
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Native `disconnectedCallback` of the HTMLElement
|
|
168
|
+
*/
|
|
89
169
|
disconnectedCallback() {
|
|
90
170
|
// remove events for elements in the component
|
|
91
171
|
this.removeEvents();
|
|
@@ -107,6 +187,11 @@ class BaseElement extends HTMLElement {
|
|
|
107
187
|
this.triggerHook('disconnected');
|
|
108
188
|
}
|
|
109
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Request and batch an asynchronous update cycle
|
|
192
|
+
* @param {PropertyUpdateOptions} options
|
|
193
|
+
* @returns {Promise<unknown>}
|
|
194
|
+
*/
|
|
110
195
|
requestUpdate(options = { notify: true }) {
|
|
111
196
|
if (options.notify === true) {
|
|
112
197
|
this.triggerHook('beforeUpdate');
|
|
@@ -136,6 +221,7 @@ class BaseElement extends HTMLElement {
|
|
|
136
221
|
* This should be called by the template component AFTER adding the template to
|
|
137
222
|
* the DOM.
|
|
138
223
|
* Here we will register the events and the refs for the element.
|
|
224
|
+
* @param {PropertyUpdateOptions} options
|
|
139
225
|
*/
|
|
140
226
|
update(options = { notify: true }) {
|
|
141
227
|
this.registerEventsAndRefs();
|
|
@@ -202,12 +288,11 @@ class BaseElement extends HTMLElement {
|
|
|
202
288
|
* Define a property on the element
|
|
203
289
|
* Will trigger watch() function when a property was changed
|
|
204
290
|
* Will trigger update() when a property was changed
|
|
291
|
+
* @param {string} property
|
|
292
|
+
* @param {any} value
|
|
293
|
+
* @param {boolean} reflectAttribute
|
|
205
294
|
*/
|
|
206
295
|
defineProperty(property, value, reflectAttribute = false) {
|
|
207
|
-
if (value instanceof Store) {
|
|
208
|
-
value.subscribe(this);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
296
|
if (this._state.hasOwnProperty(property)) {
|
|
212
297
|
// property has already been defined as an attribute nothing to do here
|
|
213
298
|
return;
|
|
@@ -223,8 +308,16 @@ class BaseElement extends HTMLElement {
|
|
|
223
308
|
this.removeAttribute(camelToDash(property));
|
|
224
309
|
}
|
|
225
310
|
|
|
311
|
+
if (hasSerializedState(this._serializationKey) && Object.keys(this.serializeState())?.includes(property)) {
|
|
312
|
+
value = this[property];
|
|
313
|
+
}
|
|
314
|
+
|
|
226
315
|
this._state[property] = value;
|
|
227
316
|
|
|
317
|
+
if (this._state[property] instanceof Store) {
|
|
318
|
+
this._state[property].subscribe(this);
|
|
319
|
+
}
|
|
320
|
+
|
|
228
321
|
Object.defineProperty(this, property, {
|
|
229
322
|
get: () => {
|
|
230
323
|
return this._state[property];
|
|
@@ -235,6 +328,8 @@ class BaseElement extends HTMLElement {
|
|
|
235
328
|
if (JSON.stringify(oldValue) !== newValueString) {
|
|
236
329
|
this._state[property] = newValue;
|
|
237
330
|
|
|
331
|
+
serializeState(this);
|
|
332
|
+
|
|
238
333
|
if (newValue instanceof Store) {
|
|
239
334
|
newValue.subscribe(this);
|
|
240
335
|
}
|
|
@@ -265,12 +360,10 @@ class BaseElement extends HTMLElement {
|
|
|
265
360
|
}
|
|
266
361
|
|
|
267
362
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* @param
|
|
271
|
-
* @param
|
|
272
|
-
* @param newValue
|
|
273
|
-
* @param oldValue
|
|
363
|
+
* Call the watch callbacks on property changes
|
|
364
|
+
* @param {string} property property that changes
|
|
365
|
+
* @param {any} newValue
|
|
366
|
+
* @param {any} oldValue
|
|
274
367
|
*/
|
|
275
368
|
callPropertyWatcher(property, newValue, oldValue) {
|
|
276
369
|
// notify watched properties (after update())
|
|
@@ -279,12 +372,11 @@ class BaseElement extends HTMLElement {
|
|
|
279
372
|
watch[property](newValue, oldValue);
|
|
280
373
|
}
|
|
281
374
|
}
|
|
375
|
+
|
|
282
376
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* @param
|
|
286
|
-
* @param property property that changes
|
|
287
|
-
* @param newValue
|
|
377
|
+
* Notify property observer via change event
|
|
378
|
+
* @param {string} property property that changes
|
|
379
|
+
* @param {any} newValue
|
|
288
380
|
*/
|
|
289
381
|
notifyPropertyChange(property, newValue) {
|
|
290
382
|
// dispatch change event
|
|
@@ -292,10 +384,14 @@ class BaseElement extends HTMLElement {
|
|
|
292
384
|
property in this._options['propertyOptions'] &&
|
|
293
385
|
this._options['propertyOptions'][property]['notify'] === true
|
|
294
386
|
) {
|
|
295
|
-
this.dispatch(`${camelToDash(property)}-changed`, newValue
|
|
387
|
+
this.dispatch(`${camelToDash(property)}-changed`, newValue);
|
|
296
388
|
}
|
|
297
389
|
}
|
|
298
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Reflects a property as attribute on the element
|
|
393
|
+
* @param {PropertyUpdateOptions} options
|
|
394
|
+
*/
|
|
299
395
|
reflectProperty(options) {
|
|
300
396
|
const { property, newValue } = options;
|
|
301
397
|
const newValueString = options.newValueString || JSON.stringify(newValue);
|
|
@@ -337,30 +433,40 @@ class BaseElement extends HTMLElement {
|
|
|
337
433
|
this.requestContext(key, value);
|
|
338
434
|
});
|
|
339
435
|
// define context provider
|
|
340
|
-
|
|
436
|
+
const providedKeys = Object.keys(this.provideProperties());
|
|
437
|
+
if (providedKeys.length > 0) {
|
|
341
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
|
+
});
|
|
342
451
|
}
|
|
343
452
|
}
|
|
344
453
|
|
|
345
454
|
/**
|
|
346
455
|
* Helper Function to initialize the context requests
|
|
347
|
-
*
|
|
348
|
-
* @param
|
|
349
|
-
* @param valueOrCallback
|
|
456
|
+
* @param {string} propertyName
|
|
457
|
+
* @param {any | function} valueOrCallback
|
|
350
458
|
*/
|
|
351
459
|
requestContext(propertyName, valueOrCallback) {
|
|
352
|
-
this.dispatch('request-context', { [propertyName]: valueOrCallback }
|
|
460
|
+
this.dispatch('request-context', { [propertyName]: valueOrCallback });
|
|
353
461
|
}
|
|
354
462
|
|
|
355
463
|
/**
|
|
356
464
|
* Internal Context Request Handler
|
|
357
465
|
* This will check if the current/receiving instance actually provides a value under the requested name.
|
|
358
466
|
* If there is something to provide it will either assign the value to the requesting element or call the callback.
|
|
359
|
-
*
|
|
360
|
-
* @param event
|
|
467
|
+
* @param {CustomEvent} event
|
|
361
468
|
*/
|
|
362
469
|
onRequestContext(event) {
|
|
363
|
-
const properties = this.properties();
|
|
364
470
|
const provideProperties = this.provideProperties();
|
|
365
471
|
|
|
366
472
|
Object.entries(event.detail ?? {}).forEach(([key, valueOrCallback]) => {
|
|
@@ -373,25 +479,38 @@ class BaseElement extends HTMLElement {
|
|
|
373
479
|
valueOrCallback(providedValue);
|
|
374
480
|
} else {
|
|
375
481
|
// assign to prop
|
|
376
|
-
|
|
482
|
+
const eventPath = event.composedPath();
|
|
483
|
+
const target = eventPath[0] || event.target;
|
|
484
|
+
target[key] = providedValue;
|
|
377
485
|
}
|
|
378
486
|
}
|
|
379
487
|
});
|
|
380
488
|
}
|
|
381
489
|
|
|
382
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Connected lifecycle hook
|
|
492
|
+
*/
|
|
383
493
|
connected() {}
|
|
384
494
|
|
|
385
|
-
|
|
495
|
+
/**
|
|
496
|
+
* BeforeUpdate lifecycle hook
|
|
497
|
+
*/
|
|
386
498
|
beforeUpdate() {}
|
|
387
499
|
|
|
388
|
-
|
|
500
|
+
/**
|
|
501
|
+
* AfterUpdate lifecycle hook
|
|
502
|
+
*/
|
|
389
503
|
afterUpdate() {}
|
|
390
504
|
|
|
391
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Disconnected lifecycle hook
|
|
507
|
+
*/
|
|
392
508
|
disconnected() {}
|
|
393
509
|
|
|
394
|
-
|
|
510
|
+
/**
|
|
511
|
+
* Triggers a lifecycle hook based on the name
|
|
512
|
+
* @param {string} name
|
|
513
|
+
*/
|
|
395
514
|
triggerHook(name) {
|
|
396
515
|
if (name in this) {
|
|
397
516
|
this[name]();
|
|
@@ -501,21 +620,66 @@ class BaseElement extends HTMLElement {
|
|
|
501
620
|
this.$refs = refsMap;
|
|
502
621
|
}
|
|
503
622
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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);
|
|
512
653
|
this.dispatchEvent(event);
|
|
513
654
|
}
|
|
514
655
|
|
|
515
|
-
|
|
656
|
+
/**
|
|
657
|
+
* Get the root element
|
|
658
|
+
* @returns {HTMLElement}
|
|
659
|
+
*/
|
|
516
660
|
getRoot() {
|
|
517
661
|
return this;
|
|
518
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
|
+
}
|
|
519
683
|
}
|
|
520
684
|
|
|
521
685
|
export { BaseElement, getClosestParentCustomElementNode, isOfSameNodeType };
|
package/src/StyledElement.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { BaseElement } from './BaseElement.js';
|
|
2
2
|
import { supportsAdoptingStyleSheets, getShadowParentOrBody } from './util/DOMHelper.js';
|
|
3
|
-
|
|
3
|
+
import { globalStylesStore } from './util/GlobalStylesStore.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (StyledElement.globalStyles && StyledElement['globalStyleSheet']) {
|
|
14
|
-
//updates already adopted global styles
|
|
15
|
-
StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
5
|
+
/**
|
|
6
|
+
* Options object for the StyledElement
|
|
7
|
+
* @typedef {Object} StyledElementOptions
|
|
8
|
+
* @extends BaseElementOptions
|
|
9
|
+
* @property {boolean} [shadowRender] - When set to true the element will render the template (if provided) in the Shadow DOM and therefore encapsulate the element and styles from the rest of the document. Default is `false`
|
|
10
|
+
* @property {[]} [styles] - Via the styles option you can add multiple styles/stylesheets to your element. Default is `[]`
|
|
11
|
+
* @property {boolean | string | string[]} [adoptGlobalStyles] - When set to true element-js will look for all style elements in the global document with and apply them before any custom/element styles inside the shadow DOM. Default is `true`.
|
|
12
|
+
*/
|
|
18
13
|
|
|
14
|
+
class StyledElement extends BaseElement {
|
|
15
|
+
/**
|
|
16
|
+
* @param {StyledElementOptions} options
|
|
17
|
+
*/
|
|
19
18
|
constructor(options) {
|
|
20
19
|
super({
|
|
21
20
|
deferUpdate: false,
|
|
@@ -27,76 +26,96 @@ class StyledElement extends BaseElement {
|
|
|
27
26
|
this._styles = [...this._options.styles, ...this.styles()];
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Overrides the `connectedCallback` to adopt optional styles when the element is connected
|
|
31
|
+
*/
|
|
30
32
|
connectedCallback() {
|
|
31
33
|
super.connectedCallback();
|
|
32
34
|
|
|
35
|
+
if (!this.constructor['elementStyleSheets']) {
|
|
36
|
+
this.constructor['elementStyleSheets'] = this._styles.map((style) => {
|
|
37
|
+
const cssStyleSheet = new CSSStyleSheet();
|
|
38
|
+
cssStyleSheet.replaceSync(style);
|
|
39
|
+
return cssStyleSheet;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
|
|
34
44
|
// adopting does only make sense in shadow dom. Fall back to append for light elements
|
|
35
45
|
this.adoptStyleSheets();
|
|
46
|
+
|
|
47
|
+
if (this._options.adoptGlobalStyles !== false) {
|
|
48
|
+
globalStylesStore.subscribe(() => {
|
|
49
|
+
this.adoptStyleSheets();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
36
52
|
}
|
|
37
53
|
}
|
|
38
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The styles method is another way to return a list of styles to be adopted when the element is connected.
|
|
57
|
+
* You can either provide a list in the constructor options or return it here.
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
39
60
|
styles() {
|
|
40
61
|
return [];
|
|
41
62
|
}
|
|
42
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Overrides the `update` method to adopt optional styles
|
|
66
|
+
* @param {PropertyUpdateOptions} options
|
|
67
|
+
*/
|
|
43
68
|
update(options) {
|
|
69
|
+
// We cannot do this in connectedCallback() since the whole template will be overridden in update/render afterward
|
|
44
70
|
if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
|
|
45
|
-
|
|
46
|
-
const appendableStyles = [...this._styles];
|
|
47
|
-
if (this._options.shadowRender && this._options.adoptGlobalStyles) {
|
|
48
|
-
appendableStyles.unshift(StyledElement.globalStyles?.textContent ?? '');
|
|
49
|
-
}
|
|
50
|
-
this.appendStyleSheets(appendableStyles);
|
|
71
|
+
this.appendStyleSheets();
|
|
51
72
|
}
|
|
52
73
|
super.update(options);
|
|
53
74
|
}
|
|
54
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Adopt stylesheets
|
|
78
|
+
*/
|
|
55
79
|
adoptStyleSheets() {
|
|
56
|
-
|
|
57
|
-
this.constructor['cssStyleSheets'] = this._styles.map((style) => {
|
|
58
|
-
const sheet = new CSSStyleSheet();
|
|
59
|
-
if (sheet && sheet.cssRules.length === 0) {
|
|
60
|
-
sheet.replaceSync(style);
|
|
61
|
-
}
|
|
62
|
-
return sheet;
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (StyledElement.globalStyles && !StyledElement['globalStyleSheet']) {
|
|
67
|
-
StyledElement['globalStyleSheet'] = new CSSStyleSheet();
|
|
68
|
-
StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
|
|
69
|
-
}
|
|
80
|
+
const adoptGlobalStyleSheets = this._options.shadowRender && this._options.adoptGlobalStyles !== false;
|
|
70
81
|
|
|
71
|
-
// adopt styles
|
|
72
|
-
// uses proposed solution for constructable stylesheets
|
|
73
|
-
// see: https://wicg.github.io/construct-stylesheets/#proposed-solution
|
|
74
82
|
this.getRoot().adoptedStyleSheets = [
|
|
75
|
-
...(
|
|
76
|
-
|
|
77
|
-
: []),
|
|
78
|
-
...this.constructor['cssStyleSheets'],
|
|
83
|
+
...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
|
|
84
|
+
...this.constructor['elementStyleSheets'],
|
|
79
85
|
];
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Custom polyfill for constructable stylesheets by appending styles to the end of an element
|
|
90
|
+
*/
|
|
91
|
+
appendStyleSheets() {
|
|
84
92
|
const parentDocument = getShadowParentOrBody(this.getRoot());
|
|
85
|
-
|
|
93
|
+
|
|
94
|
+
const adoptGlobalStyleSheets =
|
|
95
|
+
this._options.shadowRender &&
|
|
96
|
+
this._options.adoptGlobalStyles !== false &&
|
|
97
|
+
parentDocument !== globalThis.document.body;
|
|
98
|
+
|
|
99
|
+
const appendableStyles = [
|
|
100
|
+
...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
|
|
101
|
+
...this.constructor['elementStyleSheets'],
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
appendableStyles.forEach((styleSheet, index) => {
|
|
86
105
|
const identifier = this.tagName + index;
|
|
87
106
|
|
|
88
107
|
// only append stylesheet if not already appended to shadowRoot or document
|
|
89
108
|
if (!parentDocument.querySelector(`#${identifier}`)) {
|
|
90
|
-
const styleElement = document
|
|
109
|
+
const styleElement = globalThis.document?.createElement('style');
|
|
91
110
|
styleElement.id = identifier;
|
|
92
111
|
styleElement.style.display = 'none';
|
|
93
|
-
styleElement.textContent =
|
|
112
|
+
styleElement.textContent = Array.from(styleSheet.cssRules)
|
|
113
|
+
.map((rule) => rule.cssText)
|
|
114
|
+
.join('');
|
|
94
115
|
parentDocument.appendChild(styleElement);
|
|
95
116
|
}
|
|
96
117
|
});
|
|
97
118
|
}
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
StyledElement.updateGlobalStyles();
|
|
101
|
-
|
|
102
121
|
export { StyledElement };
|
package/src/TemplateElement.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { StyledElement } from './StyledElement.js';
|
|
2
2
|
import { html } from './dom-parts/html.js';
|
|
3
3
|
import { render } from './dom-parts/render.js';
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options object for the TemplateElement
|
|
7
|
+
* @typedef {Object} TemplateElementOptions
|
|
8
|
+
* @extends StyledElementOptions
|
|
9
|
+
*/
|
|
5
10
|
|
|
6
11
|
class TemplateElement extends StyledElement {
|
|
12
|
+
/**
|
|
13
|
+
* @param {TemplateElementOptions} options
|
|
14
|
+
*/
|
|
7
15
|
constructor(options) {
|
|
8
16
|
super({
|
|
9
17
|
deferUpdate: false,
|
|
@@ -16,29 +24,46 @@ class TemplateElement extends StyledElement {
|
|
|
16
24
|
...options,
|
|
17
25
|
});
|
|
18
26
|
this._template = this._options.template;
|
|
27
|
+
}
|
|
19
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Overrides the native `connectedCallback` of the HTMLElement to set up and initialize our element.
|
|
31
|
+
* This will attach a shadow DOM if the element is supposed to render in shadow DOM.
|
|
32
|
+
*/
|
|
33
|
+
connectedCallback() {
|
|
20
34
|
if (this._options.shadowRender) this.attachShadow({ mode: 'open' });
|
|
35
|
+
super.connectedCallback();
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The template method should be overridden in extending elements and return the template to be rendered to the root
|
|
40
|
+
* @returns {TemplateResult | string}
|
|
41
|
+
*/
|
|
23
42
|
template() {
|
|
24
43
|
return html``;
|
|
25
44
|
}
|
|
26
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Override update method to render the template to the root
|
|
48
|
+
* @param {PropertyUpdateOptions} options
|
|
49
|
+
*/
|
|
27
50
|
update(options) {
|
|
28
51
|
this.renderTemplate();
|
|
29
52
|
super.update(options);
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Render the template to the root
|
|
57
|
+
*/
|
|
32
58
|
renderTemplate() {
|
|
33
59
|
const template = this._template || this.template();
|
|
34
|
-
|
|
35
|
-
// just a plain string literal. no lit-html required
|
|
36
|
-
this.getRoot().innerHTML = `${template}`;
|
|
37
|
-
} else {
|
|
38
|
-
render(template, this.getRoot());
|
|
39
|
-
}
|
|
60
|
+
render(template, this.getRoot());
|
|
40
61
|
}
|
|
41
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Get the root element - either the element or the shadow root
|
|
65
|
+
* @returns {ShadowRoot | HTMLElement}
|
|
66
|
+
*/
|
|
42
67
|
getRoot() {
|
|
43
68
|
return this.shadowRoot !== null ? this.shadowRoot : this;
|
|
44
69
|
}
|
|
@@ -78,6 +78,9 @@ const processAttribute = (node, name) => {
|
|
|
78
78
|
};
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
/** @type {Map<Element, AttributePart>} */
|
|
82
|
+
const attributePartsCache = new WeakMap();
|
|
83
|
+
|
|
81
84
|
/**
|
|
82
85
|
* @param {Element} node
|
|
83
86
|
* @param {String} name
|
|
@@ -114,23 +117,27 @@ export class AttributePart extends Part {
|
|
|
114
117
|
initialValue = undefined;
|
|
115
118
|
|
|
116
119
|
/**
|
|
117
|
-
* @param {
|
|
120
|
+
* @param {Comment} node
|
|
118
121
|
* @param {String} name
|
|
119
122
|
* @param {String} initialValue
|
|
120
123
|
*/
|
|
121
124
|
constructor(node, name, initialValue) {
|
|
125
|
+
// If we have multiple attribute parts with the same name, it means we have multiple
|
|
126
|
+
// interpolations inside that attribute. Instead of creating a new part, we will return the same
|
|
127
|
+
// as before and let it defer the update until the last interpolation gets updated
|
|
128
|
+
const attributePart = attributePartsCache.get(node.nextElementSibling);
|
|
129
|
+
if (attributePart && attributePart.name === name) {
|
|
130
|
+
attributePart.interpolations++;
|
|
131
|
+
node.__part = attributePart; // add Part to comment node for debugging in the browser
|
|
132
|
+
return attributePart;
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
super();
|
|
123
136
|
this.name = name;
|
|
124
137
|
this.initialValue = initialValue;
|
|
125
138
|
this.processor = processAttributePart(node.nextElementSibling, this.name);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* @param {Node} node
|
|
130
|
-
*/
|
|
131
|
-
addNode(node) {
|
|
132
|
-
this.interpolations++;
|
|
133
|
-
// TODO: what about the comment node? It will always be the first for now...
|
|
139
|
+
node.__part = this; // add Part to comment node for debugging in the browser
|
|
140
|
+
attributePartsCache.set(node.nextElementSibling, this);
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
/**
|