@webtides/element-js 0.7.4 → 1.0.0-alpha.10

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 (51) hide show
  1. package/README.md +2 -2
  2. package/docs/README.md +174 -0
  3. package/docs/concepts/classes.md +167 -0
  4. package/docs/concepts/hierarchy.md +61 -0
  5. package/docs/concepts/lifecycle.md +45 -0
  6. package/docs/concepts/reactivity.md +14 -0
  7. package/docs/features/computed-properties.md +28 -0
  8. package/docs/features/dependency-injection-context-protocol.md +132 -0
  9. package/docs/features/events.md +155 -0
  10. package/docs/features/global-styles.md +131 -0
  11. package/docs/features/methods.md +34 -0
  12. package/docs/features/properties.md +56 -0
  13. package/docs/features/refs.md +47 -0
  14. package/docs/features/renderless.md +70 -0
  15. package/docs/features/store.md +143 -0
  16. package/docs/features/styles.md +169 -0
  17. package/docs/features/templates.md +308 -0
  18. package/docs/features/watch.md +54 -0
  19. package/docs/getting-started/upgrade-to-v1.md +243 -0
  20. package/docs/guide/guide.md +112 -0
  21. package/index.js +9 -2
  22. package/package.json +19 -27
  23. package/src/BaseElement.js +176 -86
  24. package/src/StyledElement.js +66 -55
  25. package/src/TemplateElement.js +34 -8
  26. package/src/dom-parts/AttributePart.js +167 -0
  27. package/src/dom-parts/ChildNodePart.js +329 -0
  28. package/src/dom-parts/NodePart.js +38 -0
  29. package/src/dom-parts/Part.js +12 -0
  30. package/src/dom-parts/TemplatePart.js +134 -0
  31. package/src/dom-parts/TemplateResult.js +342 -0
  32. package/src/dom-parts/directives.js +111 -0
  33. package/src/dom-parts/html.js +12 -0
  34. package/src/dom-parts/render.js +10 -0
  35. package/src/util/AttributeParser.js +62 -29
  36. package/src/util/DOMHelper.js +253 -5
  37. package/src/util/Directive.js +42 -0
  38. package/src/util/GlobalStylesStore.js +66 -0
  39. package/src/util/SerializeStateHelper.js +103 -0
  40. package/src/util/Store.js +92 -9
  41. package/src/util/defineElement.js +2 -2
  42. package/src/util/toString.js +2 -2
  43. package/CHANGELOG.md +0 -154
  44. package/dist/index.js +0 -1673
  45. package/dist/index.js.map +0 -7
  46. package/src/renderer/vanilla/TemplateElement.js +0 -50
  47. package/src/renderer/vanilla/index.js +0 -18
  48. package/src/renderer/vanilla/util/directives.js +0 -78
  49. package/src/renderer/vanilla/util/html.js +0 -65
  50. package/src/renderer/vanilla/util/render.js +0 -149
  51. package/src/util/i18n.js +0 -20
@@ -1,11 +1,54 @@
1
1
  import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from './util/AttributeParser.js';
2
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} PropertyOptions
28
+ * @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`
29
+ * @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`
30
+ * @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`
31
+ */
32
+
33
+ /**
34
+ * Options object for updating properties
35
+ * @typedef {Object} PropertyUpdateOptions
36
+ * @property {boolean} [notify] - Whether the update should trigger the lifecycle methods. The default is `true`
37
+ * @property {string} property - The property name
38
+ * @property {any} newValue - The new value for the property
39
+ * @property {string} newValueString - The new value as `string` for the property (for comparison reasons)
40
+ * @property {any} oldValue - The old value for the property
41
+ */
42
+
43
+ /**
44
+ * @implements {Serializable}
45
+ */
8
46
  class BaseElement extends HTMLElement {
47
+ _serializationKey;
48
+
49
+ /**
50
+ * @param {BaseElementOptions} options
51
+ */
9
52
  constructor(options = {}) {
10
53
  super();
11
54
  this.$refs = {};
@@ -27,37 +70,40 @@ class BaseElement extends HTMLElement {
27
70
  propertyOptions: {},
28
71
  ...options,
29
72
  };
73
+ }
30
74
 
75
+ /**
76
+ * Overrides the native `connectedCallback` of the HTMLElement to set up and initialize our element.
77
+ * This will define the attributes and properties as reactive getters and setter,
78
+ * register observers, refs and events.
79
+ */
80
+ connectedCallback() {
31
81
  if (this.hasAttribute('defer-update')) {
32
82
  this._options.deferUpdate = true;
33
83
  }
34
- if (this.hasAttribute('defer-connected')) {
35
- this._options.deferConnected = true;
36
- }
37
84
 
38
- if (options.childListUpdate !== undefined && options.childListUpdate !== null) {
39
- this._options.mutationObserverOptions.childList = options.childListUpdate;
40
- console.warn(
41
- `[${this.localName}] Using the "childListUpdate" option is deprecated and will be removed before 1.0! Please use the "mutationObserverOptions" dictionary instead. See the docs for more info`,
42
- );
43
- }
44
- }
45
-
46
- connectedCallback() {
47
- if (this._options.deferConnected) {
85
+ if (this.hasAttribute('defer-connected') || this._options.deferConnected) {
48
86
  this._options.deferConnected = false;
87
+ this.removeAttribute('defer-connected');
49
88
  return;
50
89
  }
51
90
 
91
+ if (this.hasAttribute('ejs:key')) {
92
+ this._serializationKey = this.getAttribute('ejs:key');
93
+ deserializeState(this);
94
+ } else {
95
+ this._serializationKey = globalThis.crypto.randomUUID();
96
+ if (globalThis.elementJsConfig?.serializeState) {
97
+ this.setAttribute('ejs:key', this._serializationKey);
98
+ }
99
+ }
100
+
52
101
  // define all attributes to "this" as properties
53
102
  this.defineAttributesAsProperties();
54
103
 
55
104
  // define all properties to "this"
56
105
  this.defineProperties();
57
106
 
58
- // define all computed properties to "this"
59
- this.defineComputedProperties();
60
-
61
107
  // define context
62
108
  this.definePropertyInjection();
63
109
 
@@ -65,17 +111,21 @@ class BaseElement extends HTMLElement {
65
111
  this.defineObserver();
66
112
 
67
113
  if (this._options.deferUpdate) {
68
- // don't updates/render, but register refs and events
114
+ // don't update/render, but register refs and events
69
115
  this.registerEventsAndRefs();
70
116
 
71
117
  this.triggerHook('connected');
72
118
  } else {
73
119
  this.requestUpdate({ notify: false }).then(() => {
74
120
  this.triggerHook('connected');
121
+ this.triggerHook('afterUpdate');
75
122
  });
76
123
  }
77
124
  }
78
125
 
126
+ /**
127
+ * Define a MutationObserver on the element to get notified on attribute changes and childList updates according to the options specified via the constructor options.
128
+ */
79
129
  defineObserver() {
80
130
  // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#Example_usage
81
131
  this._mutationObserver = new MutationObserver((mutations) => {
@@ -108,6 +158,9 @@ class BaseElement extends HTMLElement {
108
158
  });
109
159
  }
110
160
 
161
+ /**
162
+ * Native `disconnectedCallback` of the HTMLElement
163
+ */
111
164
  disconnectedCallback() {
112
165
  // remove events for elements in the component
113
166
  this.removeEvents();
@@ -129,6 +182,11 @@ class BaseElement extends HTMLElement {
129
182
  this.triggerHook('disconnected');
130
183
  }
131
184
 
185
+ /**
186
+ * Request and batch an asynchronous update cycle
187
+ * @param {PropertyUpdateOptions} options
188
+ * @returns {Promise<unknown>}
189
+ */
132
190
  requestUpdate(options = { notify: true }) {
133
191
  if (options.notify === true) {
134
192
  this.triggerHook('beforeUpdate');
@@ -158,6 +216,7 @@ class BaseElement extends HTMLElement {
158
216
  * This should be called by the template component AFTER adding the template to
159
217
  * the DOM.
160
218
  * Here we will register the events and the refs for the element.
219
+ * @param {PropertyUpdateOptions} options
161
220
  */
162
221
  update(options = { notify: true }) {
163
222
  this.registerEventsAndRefs();
@@ -224,12 +283,11 @@ class BaseElement extends HTMLElement {
224
283
  * Define a property on the element
225
284
  * Will trigger watch() function when a property was changed
226
285
  * Will trigger update() when a property was changed
286
+ * @param {string} property
287
+ * @param {any} value
288
+ * @param {boolean} reflectAttribute
227
289
  */
228
290
  defineProperty(property, value, reflectAttribute = false) {
229
- if (value instanceof Store) {
230
- value.subscribe(this);
231
- }
232
-
233
291
  if (this._state.hasOwnProperty(property)) {
234
292
  // property has already been defined as an attribute nothing to do here
235
293
  return;
@@ -245,8 +303,16 @@ class BaseElement extends HTMLElement {
245
303
  this.removeAttribute(camelToDash(property));
246
304
  }
247
305
 
306
+ if (hasSerializedState(this._serializationKey) && Object.keys(this.serializeState())?.includes(property)) {
307
+ value = this[property];
308
+ }
309
+
248
310
  this._state[property] = value;
249
311
 
312
+ if (this._state[property] instanceof Store) {
313
+ this._state[property].subscribe(this);
314
+ }
315
+
250
316
  Object.defineProperty(this, property, {
251
317
  get: () => {
252
318
  return this._state[property];
@@ -257,6 +323,8 @@ class BaseElement extends HTMLElement {
257
323
  if (JSON.stringify(oldValue) !== newValueString) {
258
324
  this._state[property] = newValue;
259
325
 
326
+ serializeState(this);
327
+
260
328
  if (newValue instanceof Store) {
261
329
  newValue.subscribe(this);
262
330
  }
@@ -287,12 +355,10 @@ class BaseElement extends HTMLElement {
287
355
  }
288
356
 
289
357
  /**
290
- * call the watch callbacks on property changes
291
- *
292
- * @param context is the elements instance
293
- * @param property property that changes
294
- * @param newValue
295
- * @param oldValue
358
+ * Call the watch callbacks on property changes
359
+ * @param {string} property property that changes
360
+ * @param {any} newValue
361
+ * @param {any} oldValue
296
362
  */
297
363
  callPropertyWatcher(property, newValue, oldValue) {
298
364
  // notify watched properties (after update())
@@ -303,11 +369,9 @@ class BaseElement extends HTMLElement {
303
369
  }
304
370
 
305
371
  /**
306
- * notify property observer via change event
307
- *
308
- * @param context is the elements instance
309
- * @param property property that changes
310
- * @param newValue
372
+ * Notify property observer via change event
373
+ * @param {string} property property that changes
374
+ * @param {any} newValue
311
375
  */
312
376
  notifyPropertyChange(property, newValue) {
313
377
  // dispatch change event
@@ -315,10 +379,14 @@ class BaseElement extends HTMLElement {
315
379
  property in this._options['propertyOptions'] &&
316
380
  this._options['propertyOptions'][property]['notify'] === true
317
381
  ) {
318
- this.dispatch(`${camelToDash(property)}-changed`, newValue, true);
382
+ this.dispatch(`${camelToDash(property)}-changed`, newValue);
319
383
  }
320
384
  }
321
385
 
386
+ /**
387
+ * Reflects a property as attribute on the element
388
+ * @param {PropertyUpdateOptions} options
389
+ */
322
390
  reflectProperty(options) {
323
391
  const { property, newValue } = options;
324
392
  const newValueString = options.newValueString || JSON.stringify(newValue);
@@ -380,20 +448,18 @@ class BaseElement extends HTMLElement {
380
448
 
381
449
  /**
382
450
  * Helper Function to initialize the context requests
383
- *
384
- * @param propertyName
385
- * @param valueOrCallback
451
+ * @param {string} propertyName
452
+ * @param {any | function} valueOrCallback
386
453
  */
387
454
  requestContext(propertyName, valueOrCallback) {
388
- this.dispatch('request-context', { [propertyName]: valueOrCallback }, true, true, true);
455
+ this.dispatch('request-context', { [propertyName]: valueOrCallback });
389
456
  }
390
457
 
391
458
  /**
392
459
  * Internal Context Request Handler
393
460
  * This will check if the current/receiving instance actually provides a value under the requested name.
394
461
  * If there is something to provide it will either assign the value to the requesting element or call the callback.
395
- *
396
- * @param event
462
+ * @param {CustomEvent} event
397
463
  */
398
464
  onRequestContext(event) {
399
465
  const provideProperties = this.provideProperties();
@@ -416,33 +482,31 @@ class BaseElement extends HTMLElement {
416
482
  });
417
483
  }
418
484
 
419
- // Deprecated
420
- hooks() {
421
- return {};
422
- }
423
-
424
- // Connected lifecycle hook
485
+ /**
486
+ * Connected lifecycle hook
487
+ */
425
488
  connected() {}
426
489
 
427
- // BeforeUpdate lifecycle hook
490
+ /**
491
+ * BeforeUpdate lifecycle hook
492
+ */
428
493
  beforeUpdate() {}
429
494
 
430
- // AfterUpdate lifecycle hook
495
+ /**
496
+ * AfterUpdate lifecycle hook
497
+ */
431
498
  afterUpdate() {}
432
499
 
433
- // Disconnected lifecycle hook
500
+ /**
501
+ * Disconnected lifecycle hook
502
+ */
434
503
  disconnected() {}
435
504
 
436
- // Triggers a lifecycle hook based on the name
505
+ /**
506
+ * Triggers a lifecycle hook based on the name
507
+ * @param {string} name
508
+ */
437
509
  triggerHook(name) {
438
- if (this.hooks && name in this.hooks()) {
439
- console.warn(
440
- `[${this.localName}] Using the hooks() map for lifecycle hooks is deprecated! Please overwrite the existing lifecycle hook functions. See the docs for more info`,
441
- );
442
- this.hooks()[name]();
443
- return;
444
- }
445
-
446
510
  if (name in this) {
447
511
  this[name]();
448
512
  }
@@ -459,25 +523,6 @@ class BaseElement extends HTMLElement {
459
523
  return {};
460
524
  }
461
525
 
462
- // Deprecated
463
- computed() {
464
- return {};
465
- }
466
-
467
- // Deprecated
468
- defineComputedProperties() {
469
- Object.keys(this.computed()).forEach((prop) => {
470
- if (!this.hasOwnProperty(prop)) {
471
- console.warn(
472
- `[${this.localName}] Using the computed() map for computed properties is deprecated! Please use regular JS getters and return the computed value. See the docs for more info`,
473
- );
474
- Object.defineProperty(this, prop, {
475
- get: () => this.computed()[prop](),
476
- });
477
- }
478
- });
479
- }
480
-
481
526
  /**
482
527
  * Return a map of events that should be registered on the element.
483
528
  * eg. { '.link': {
@@ -570,21 +615,66 @@ class BaseElement extends HTMLElement {
570
615
  this.$refs = refsMap;
571
616
  }
572
617
 
573
- // Helper function for dispatching custom events
574
- dispatch(name, data, bubble = false, cancelable = false, composed = false) {
575
- const event = new CustomEvent(name, {
576
- bubbles: bubble,
577
- cancelable: cancelable,
578
- composed: composed,
579
- detail: data,
580
- });
618
+ /**
619
+ * Helper function for dispatching custom events
620
+ * @param {string} name
621
+ * @param {Object} [data]
622
+ * @param {CustomEventInit} [options]
623
+ * @param {boolean} [cancelable] - DEPRECATED
624
+ * @param {boolean} [composed] - DEPRECATED
625
+ */
626
+ dispatch(name, data, options, cancelable = false, composed = false) {
627
+ let eventInit = {};
628
+
629
+ if (options !== undefined && typeof options === 'boolean') {
630
+ eventInit['bubbles'] = options;
631
+ eventInit['cancelable'] = cancelable;
632
+ eventInit['composed'] = composed;
633
+ eventInit['detail'] = data;
634
+ console.warn(
635
+ `[${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`,
636
+ );
637
+ } else {
638
+ eventInit = {
639
+ bubbles: true,
640
+ cancelable: true,
641
+ composed: true,
642
+ detail: data,
643
+ ...options,
644
+ };
645
+ }
646
+
647
+ const event = new CustomEvent(name, eventInit);
581
648
  this.dispatchEvent(event);
582
649
  }
583
650
 
584
- // Get the root element
651
+ /**
652
+ * Get the root element
653
+ * @returns {HTMLElement}
654
+ */
585
655
  getRoot() {
586
656
  return this;
587
657
  }
658
+
659
+ /**
660
+ * This method will be used to serialize this elements state.
661
+ * By default, it will generate an object with all properties from this.properties().
662
+ * You can override this and return a different object with more/less/different properties.
663
+ * @return {{[string: any]: *}}
664
+ */
665
+ serializeState() {
666
+ const keys = Object.keys(this.properties());
667
+ return Object.fromEntries(keys.map((key) => [key, this[key]]));
668
+ }
669
+
670
+ /**
671
+ * This method will be used to restore properties on the element from a previously serialized state.
672
+ * You can override this and handle the deserialization differently if the default is not enough.
673
+ * @param {{[string: any]: *}} serializedState
674
+ */
675
+ restoreState(serializedState) {
676
+ Object.assign(this, serializedState);
677
+ }
588
678
  }
589
679
 
590
680
  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,84 +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();
36
- } else if (this._options.shadowRender && window.ShadyCSS !== undefined) {
37
- // if shadowRoot is polyfilled we use ShadyCSS to copy scoped styles to <head>
38
- window.ShadyCSS.ScopingShim.prepareAdoptedCssText(this._styles, this.localName);
39
- }
40
46
 
41
- // if shadowRoot is polyfilled - scope element template
42
- if (window.ShadyCSS !== undefined) {
43
- window.ShadyCSS.styleElement(this);
47
+ if (this._options.adoptGlobalStyles !== false) {
48
+ globalStylesStore.subscribe(() => {
49
+ this.adoptStyleSheets();
50
+ });
51
+ }
44
52
  }
45
53
  }
46
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
+ */
47
60
  styles() {
48
61
  return [];
49
62
  }
50
63
 
64
+ /**
65
+ * Overrides the `update` method to adopt optional styles
66
+ * @param {PropertyUpdateOptions} options
67
+ */
51
68
  update(options) {
69
+ // We cannot do this in connectedCallback() since the whole template will be overridden in update/render afterward
52
70
  if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
53
- // append stylesheets to template if not already adopted
54
- const appendableStyles = [...this._styles];
55
- if (this._options.shadowRender && this._options.adoptGlobalStyles && !window.ShadyCSS) {
56
- appendableStyles.unshift(StyledElement.globalStyles?.textContent ?? '');
57
- }
58
- this.appendStyleSheets(appendableStyles);
71
+ this.appendStyleSheets();
59
72
  }
60
73
  super.update(options);
61
74
  }
62
75
 
76
+ /**
77
+ * Adopt stylesheets
78
+ */
63
79
  adoptStyleSheets() {
64
- if (!this.constructor['cssStyleSheets']) {
65
- this.constructor['cssStyleSheets'] = this._styles.map((style) => {
66
- const sheet = new CSSStyleSheet();
67
- if (sheet && sheet.cssRules.length === 0) {
68
- sheet.replaceSync(style);
69
- }
70
- return sheet;
71
- });
72
- }
73
-
74
- if (StyledElement.globalStyles && !StyledElement['globalStyleSheet']) {
75
- StyledElement['globalStyleSheet'] = new CSSStyleSheet();
76
- StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
77
- }
80
+ const adoptGlobalStyleSheets = this._options.shadowRender && this._options.adoptGlobalStyles !== false;
78
81
 
79
- // adopt styles
80
- // uses proposed solution for constructable stylesheets
81
- // see: https://wicg.github.io/construct-stylesheets/#proposed-solution
82
82
  this.getRoot().adoptedStyleSheets = [
83
- ...(this._options.shadowRender && this._options.adoptGlobalStyles && StyledElement['globalStyleSheet']
84
- ? [StyledElement['globalStyleSheet']]
85
- : []),
86
- ...this.constructor['cssStyleSheets'],
83
+ ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
84
+ ...this.constructor['elementStyleSheets'],
87
85
  ];
88
86
  }
89
87
 
90
- // custom polyfill for constructable stylesheets by appending styles to the end of an element
91
- appendStyleSheets(styles) {
88
+ /**
89
+ * Custom polyfill for constructable stylesheets by appending styles to the end of an element
90
+ */
91
+ appendStyleSheets() {
92
92
  const parentDocument = getShadowParentOrBody(this.getRoot());
93
- 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) => {
94
105
  const identifier = this.tagName + index;
95
106
 
96
107
  // only append stylesheet if not already appended to shadowRoot or document
97
108
  if (!parentDocument.querySelector(`#${identifier}`)) {
98
- const styleElement = document.createElement('style');
109
+ const styleElement = globalThis.document?.createElement('style');
99
110
  styleElement.id = identifier;
100
111
  styleElement.style.display = 'none';
101
- styleElement.textContent = style;
112
+ styleElement.textContent = Array.from(styleSheet.cssRules)
113
+ .map((rule) => rule.cssText)
114
+ .join('');
102
115
  parentDocument.appendChild(styleElement);
103
116
  }
104
117
  });
105
118
  }
106
119
  }
107
120
 
108
- StyledElement.updateGlobalStyles();
109
-
110
121
  export { StyledElement };
@@ -1,9 +1,17 @@
1
1
  import { StyledElement } from './StyledElement.js';
2
- import { html } from 'lit-html';
3
- import { render } from 'lit-html/lib/shady-render.js';
4
- export { i18n } from './util/i18n.js';
2
+ import { html } from './dom-parts/html.js';
3
+ import { render } from './dom-parts/render.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,33 +24,51 @@ 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}
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
60
  if (typeof template === 'string') {
35
61
  // just a plain string literal. no lit-html required
36
62
  this.getRoot().innerHTML = `${template}`;
37
63
  } else {
38
- // render via lit-html
39
- render(html` ${template} `, this.getRoot(), {
40
- scopeName: this.localName,
41
- eventContext: this,
42
- });
64
+ render(template, this.getRoot());
43
65
  }
44
66
  }
45
67
 
68
+ /**
69
+ * Get the root element - either the element or the shadow root
70
+ * @returns {ShadowRoot | HTMLElement}
71
+ */
46
72
  getRoot() {
47
73
  return this.shadowRoot !== null ? this.shadowRoot : this;
48
74
  }