@webtides/element-js 1.2.5 → 1.2.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 CHANGED
@@ -94,7 +94,7 @@ class ExampleElement extends BaseElement {
94
94
  // lifecycle hook
95
95
  connected() {
96
96
  this.greet();
97
- }
97
+ }
98
98
 
99
99
  // reactive attributes/properties
100
100
  properties() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.2.5",
3
+ "version": "1.2.11",
4
4
  "module": "index.js",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -19,7 +19,10 @@
19
19
  "./src/util/*": "./src/util/*.js",
20
20
  "./src/dom-parts/*": "./src/dom-parts/*.js"
21
21
  },
22
- "repository": "https://github.com/webtides/element-js.git",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/webtides/element-js.git"
25
+ },
23
26
  "author": "@webtides",
24
27
  "license": "MIT",
25
28
  "keywords": [
@@ -1,5 +1,10 @@
1
1
  import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from './util/AttributeParser.js';
2
- import { getAllElementChildren, getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
2
+ import {
3
+ getAllComposedElementChildren,
4
+ getAllElementChildren,
5
+ getClosestParentCustomElementNode,
6
+ isOfSameNodeType,
7
+ } from './util/DOMHelper.js';
3
8
  import { Store } from './util/Store.js';
4
9
  import { serializeState, deserializeState, hasSerializedState } from './util/SerializeStateHelper.js';
5
10
  import { randomUUID } from './util/crypto.js';
@@ -126,11 +131,15 @@ class BaseElement extends HTMLElement {
126
131
 
127
132
  this.triggerHook('connected');
128
133
  this._internals.states.add('connected');
134
+ // verification must happen after connection
135
+ this.verifyProvidedProperties();
129
136
  } else {
130
137
  this.requestUpdate({ notify: false }).then(() => {
131
138
  this.triggerHook('connected');
132
139
  this._internals.states.add('connected');
133
140
  this.triggerHook('afterUpdate');
141
+ // verification must happen after connection
142
+ this.verifyProvidedProperties();
134
143
  });
135
144
  }
136
145
  }
@@ -194,9 +203,6 @@ class BaseElement extends HTMLElement {
194
203
  // release dom references
195
204
  this.$refs = {};
196
205
 
197
- // clear state
198
- this._state = {};
199
-
200
206
  // cancel pending updates
201
207
  if (this._batchUpdate) {
202
208
  cancelAnimationFrame(this._batchUpdate);
@@ -292,13 +298,14 @@ class BaseElement extends HTMLElement {
292
298
  * Will trigger update() when a property was changed
293
299
  */
294
300
  defineProperties() {
295
- Object.keys({ ...this.injectProperties(), ...this.properties() }).forEach((prop) => {
301
+ const properties = this.properties();
302
+ Object.keys({ ...this.injectProperties(), ...properties }).forEach((prop) => {
296
303
  // when mixing shadow dom elements with light dom elements and nesting custom elements
297
304
  // it might occur that properties where set on an element before it was
298
305
  // registered or connected. To avoid such timing issues we check
299
306
  // if a value was set for that specific property on the
300
307
  // prototype before assigning a default value
301
- const value = this[prop] || this.properties()[prop];
308
+ const value = this[prop] ?? properties[prop];
302
309
  this.defineProperty(prop, value);
303
310
  });
304
311
  }
@@ -312,11 +319,6 @@ class BaseElement extends HTMLElement {
312
319
  * @param {boolean} isAttribute
313
320
  */
314
321
  defineProperty(property, value, isAttribute = false) {
315
- if (this._state.hasOwnProperty(property)) {
316
- // property has already been defined as an attribute nothing to do here
317
- return;
318
- }
319
-
320
322
  // if property did not come from an attribute but has the option to reflect // enabled or custom fn
321
323
  if (!isAttribute && this._options.propertyOptions[property]?.reflect) {
322
324
  this.reflectProperty({ property: property, newValue: value });
@@ -460,14 +462,35 @@ class BaseElement extends HTMLElement {
460
462
  const providedKeys = Object.keys(this.provideProperties());
461
463
  if (providedKeys.length > 0) {
462
464
  this.addEventListener('request-context', this.onRequestContext);
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Verifies provided context after the element has finished connecting.
470
+ *
471
+ * This is used as a failover step for late providers and lifecycle race conditions,
472
+ * especially when a provided value is assigned inside `connected()` and the initial
473
+ * context request was resolved before that mutation happened.
474
+ *
475
+ * By replaying matching requests here we ensure children receive the final provider
476
+ * value instead of being stuck with an early placeholder like `null`.
477
+ */
478
+
479
+ verifyProvidedProperties() {
480
+ // define context provider
481
+ const providedProperties = this.provideProperties();
482
+ const providedKeys = Object.keys(providedProperties);
483
+ if (providedKeys.length > 0) {
484
+ // take both light and shadow dom into account.
485
+ const composedChildren = getAllComposedElementChildren(this, BaseElement);
463
486
  // check if there are already connected elements in child dom and restart requests
464
- getAllElementChildren(this.getRoot(), BaseElement).forEach((customChild) => {
487
+ composedChildren.forEach((customChild) => {
465
488
  // if injectProperties?.() is defined it means that the child got connected BEFORE the parent (Runtime Issue)
466
489
  const requestedProperties = customChild.injectProperties?.() ?? {};
467
490
  Object.entries(requestedProperties).forEach(([key, value]) => {
468
491
  // iterate over requested properties to find missed context requests
469
- if (providedKeys.includes(key)) {
470
- // owns the provided key -> replay request
492
+ if (providedKeys.includes(key) && customChild[key] !== providedProperties[key]) {
493
+ // owns the provided key -> replay request
471
494
  customChild.requestContext(key, value);
472
495
  }
473
496
  });
@@ -39,17 +39,6 @@ class StyledElement extends BaseElement {
39
39
  return cssStyleSheet;
40
40
  });
41
41
  }
42
-
43
- if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
44
- // adopting does only make sense in shadow dom. Fall back to append for light elements
45
- this.adoptStyleSheets();
46
-
47
- if (this._options.adoptGlobalStyles !== false) {
48
- globalStylesStore.subscribe(() => {
49
- this.adoptStyleSheets();
50
- });
51
- }
52
- }
53
42
  }
54
43
 
55
44
  /**
@@ -73,6 +62,26 @@ class StyledElement extends BaseElement {
73
62
  super.update(options);
74
63
  }
75
64
 
65
+ /**
66
+ * Start the adoption of external stylesheets
67
+ * adopt global styles from document
68
+ * subscribe to globalStylesStore for updates
69
+ * @returns { void }
70
+ */
71
+
72
+ initStyleAdoption() {
73
+ if (this._options.shadowRender && supportsAdoptingStyleSheets()) {
74
+ // adopting does only make sense in shadow dom. Fall back to append for light elements
75
+ this.adoptStyleSheets();
76
+
77
+ if (this._options.adoptGlobalStyles !== false) {
78
+ globalStylesStore.subscribe(() => {
79
+ this.adoptStyleSheets();
80
+ });
81
+ }
82
+ }
83
+ }
84
+
76
85
  /**
77
86
  * Adopt stylesheets
78
87
  */
@@ -81,7 +90,7 @@ class StyledElement extends BaseElement {
81
90
 
82
91
  this.getRoot().adoptedStyleSheets = [
83
92
  ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
84
- ...this.constructor['elementStyleSheets'],
93
+ ...(this.constructor?.['elementStyleSheets'] ?? []),
85
94
  ];
86
95
  }
87
96
 
@@ -98,7 +107,7 @@ class StyledElement extends BaseElement {
98
107
 
99
108
  const appendableStyles = [
100
109
  ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
101
- ...this.constructor['elementStyleSheets'],
110
+ ...(this.constructor?.['elementStyleSheets'] ?? []),
102
111
  ];
103
112
 
104
113
  appendableStyles.forEach((styleSheet, index) => {
@@ -28,10 +28,8 @@ class TemplateElement extends StyledElement {
28
28
 
29
29
  /**
30
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
31
  */
33
32
  connectedCallback() {
34
- if (this._options.shadowRender && !this.shadowRoot) this.attachShadow({ mode: 'open' });
35
33
  super.connectedCallback();
36
34
  }
37
35
 
@@ -54,8 +52,16 @@ class TemplateElement extends StyledElement {
54
52
 
55
53
  /**
56
54
  * Render the template to the root
55
+ * This will attach a shadow DOM if the element is supposed to render in shadow DOM.
57
56
  */
58
57
  renderTemplate() {
58
+ const firstRender = this._options.shadowRender && !this.shadowRoot;
59
+ if (firstRender) {
60
+ this.attachShadow({ mode: 'open' });
61
+ // init external stylesheets
62
+ this.initStyleAdoption();
63
+ }
64
+
59
65
  const template = this._template || this.template();
60
66
  render(template, this.getRoot());
61
67
  }
@@ -60,7 +60,7 @@ export function getShadowParentOrBody(element) {
60
60
  return document.body;
61
61
  }
62
62
 
63
- // TODO: add function for getClosestParentOfNodeType('custom-element')
63
+ // TODO: add function for getClosestParentOfNo deType('custom-element')
64
64
 
65
65
  /**
66
66
  * Get the closest/first parent node that is a custom element
@@ -76,7 +76,7 @@ export function getClosestParentCustomElementNode(domNode) {
76
76
  }
77
77
 
78
78
  /**
79
- * Get all
79
+ * Get all custom element children of a DOM node
80
80
  * @param {Node} domNode
81
81
  * @param {{prototype: HTMLElement, new()}} instanceOfFilter
82
82
  * @return {Node[]}
@@ -95,6 +95,20 @@ export function getAllElementChildren(domNode, instanceOfFilter = HTMLElement) {
95
95
  return customElements;
96
96
  }
97
97
 
98
+ /**
99
+ * Get all custom element children of a DOM node, including children inside the node's shadow root.
100
+ *
101
+ * @param {Node} domNode - The root node to start the traversal from.
102
+ * @param {{prototype: HTMLElement, new(): HTMLElement}} [instanceOfFilter=HTMLElement] - Constructor used to filter matching custom elements.
103
+ * @return {Set<HTMLElement>} A set of matching custom element descendants.
104
+ */
105
+ export function getAllComposedElementChildren(domNode, instanceOfFilter = HTMLElement) {
106
+ return new Set([
107
+ ...getAllElementChildren(domNode, instanceOfFilter),
108
+ ...(domNode.shadowRoot ? getAllElementChildren(domNode.shadowRoot, instanceOfFilter) : []),
109
+ ]);
110
+ }
111
+
98
112
  /**
99
113
  * Checks whether two nodes are of the same node type
100
114
  * @param {Node} nodeA