@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.
Files changed (43) hide show
  1. package/docs/README.md +174 -0
  2. package/docs/concepts/classes.md +167 -0
  3. package/docs/concepts/hierarchy.md +61 -0
  4. package/docs/concepts/lifecycle.md +45 -0
  5. package/docs/concepts/reactivity.md +14 -0
  6. package/docs/features/computed-properties.md +28 -0
  7. package/docs/features/dependency-injection-context-protocol.md +132 -0
  8. package/docs/features/events.md +155 -0
  9. package/docs/features/global-styles.md +131 -0
  10. package/docs/features/methods.md +34 -0
  11. package/docs/features/properties.md +56 -0
  12. package/docs/features/refs.md +47 -0
  13. package/docs/features/renderless.md +70 -0
  14. package/docs/features/store.md +143 -0
  15. package/docs/features/styles.md +169 -0
  16. package/docs/features/templates.md +308 -0
  17. package/docs/features/watch.md +54 -0
  18. package/docs/getting-started/upgrade-to-v1.md +243 -0
  19. package/docs/guide/guide.md +112 -0
  20. package/index.js +9 -2
  21. package/package.json +15 -19
  22. package/src/BaseElement.js +206 -42
  23. package/src/StyledElement.js +67 -48
  24. package/src/TemplateElement.js +32 -7
  25. package/src/dom-parts/AttributePart.js +16 -9
  26. package/src/dom-parts/ChildNodePart.js +8 -22
  27. package/src/dom-parts/NodePart.js +1 -0
  28. package/src/dom-parts/TemplatePart.js +19 -25
  29. package/src/dom-parts/TemplateResult.js +46 -35
  30. package/src/dom-parts/directives.js +43 -16
  31. package/src/dom-parts/html.js +1 -1
  32. package/src/dom-parts/render.js +6 -1
  33. package/src/util/AttributeParser.js +54 -21
  34. package/src/util/DOMHelper.js +63 -14
  35. package/src/util/Directive.js +21 -1
  36. package/src/util/GlobalStylesStore.js +66 -0
  37. package/src/util/SerializeStateHelper.js +103 -0
  38. package/src/util/Store.js +111 -15
  39. package/src/util/defineElement.js +2 -2
  40. package/src/util/toString.js +2 -2
  41. package/dist/index.js +0 -1330
  42. package/dist/index.js.map +0 -7
  43. package/src/util/i18n.js +0 -20
@@ -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.hasAttribute('defer-update') || this._options.deferUpdate) {
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
- * call the watch callbacks on property changes
269
- *
270
- * @param context is the elements instance
271
- * @param property property that changes
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
- * notify property observer via change event
284
- *
285
- * @param context is the elements instance
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, true);
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
- if (Object.keys(this.provideProperties()).length > 0) {
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 propertyName
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 }, true);
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
- event.target[key] = providedValue;
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
- // Connected lifecycle hook
490
+ /**
491
+ * Connected lifecycle hook
492
+ */
383
493
  connected() {}
384
494
 
385
- // BeforeUpdate lifecycle hook
495
+ /**
496
+ * BeforeUpdate lifecycle hook
497
+ */
386
498
  beforeUpdate() {}
387
499
 
388
- // AfterUpdate lifecycle hook
500
+ /**
501
+ * AfterUpdate lifecycle hook
502
+ */
389
503
  afterUpdate() {}
390
504
 
391
- // Disconnected lifecycle hook
505
+ /**
506
+ * Disconnected lifecycle hook
507
+ */
392
508
  disconnected() {}
393
509
 
394
- // Triggers a lifecycle hook based on the name
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
- // Helper function for dispatching custom events
505
- dispatch(name, data, bubble = false, cancelable = false, composed = false) {
506
- const event = new CustomEvent(name, {
507
- bubbles: bubble,
508
- cancelable: cancelable,
509
- composed: composed,
510
- detail: data,
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
- // Get the root element
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 };
@@ -1,21 +1,20 @@
1
1
  import { BaseElement } from './BaseElement.js';
2
2
  import { supportsAdoptingStyleSheets, getShadowParentOrBody } from './util/DOMHelper.js';
3
- export { i18n } from './util/i18n.js';
3
+ import { globalStylesStore } from './util/GlobalStylesStore.js';
4
4
 
5
- class StyledElement extends BaseElement {
6
- static globalStyles = null;
7
-
8
- static updateGlobalStyles() {
9
- // this is a runtime dependency so that every shadow dom can make use of global css
10
- // we assume these styles to be inlined into the document
11
- StyledElement.globalStyles = document.getElementById('globalStyles');
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
- // append stylesheets to template if not already adopted
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
- if (!this.constructor['cssStyleSheets']) {
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
- ...(this._options.shadowRender && this._options.adoptGlobalStyles && StyledElement['globalStyleSheet']
76
- ? [StyledElement['globalStyleSheet']]
77
- : []),
78
- ...this.constructor['cssStyleSheets'],
83
+ ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
84
+ ...this.constructor['elementStyleSheets'],
79
85
  ];
80
86
  }
81
87
 
82
- // custom polyfill for constructable stylesheets by appending styles to the end of an element
83
- appendStyleSheets(styles) {
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
- styles.forEach((style, index) => {
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.createElement('style');
109
+ const styleElement = globalThis.document?.createElement('style');
91
110
  styleElement.id = identifier;
92
111
  styleElement.style.display = 'none';
93
- styleElement.textContent = style;
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 };
@@ -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
- export { i18n } from './util/i18n.js';
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
- if (typeof template === 'string') {
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 {Node} node
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
  /**