@webtides/element-js 1.2.6 → 1.2.12

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.6",
3
+ "version": "1.2.12",
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
  }
@@ -289,13 +298,14 @@ class BaseElement extends HTMLElement {
289
298
  * Will trigger update() when a property was changed
290
299
  */
291
300
  defineProperties() {
292
- Object.keys({ ...this.injectProperties(), ...this.properties() }).forEach((prop) => {
301
+ const properties = this.properties();
302
+ Object.keys({ ...this.injectProperties(), ...properties }).forEach((prop) => {
293
303
  // when mixing shadow dom elements with light dom elements and nesting custom elements
294
304
  // it might occur that properties where set on an element before it was
295
305
  // registered or connected. To avoid such timing issues we check
296
306
  // if a value was set for that specific property on the
297
307
  // prototype before assigning a default value
298
- const value = this[prop] ?? this.properties()[prop];
308
+ const value = this[prop] ?? properties[prop];
299
309
  this.defineProperty(prop, value);
300
310
  });
301
311
  }
@@ -452,14 +462,35 @@ class BaseElement extends HTMLElement {
452
462
  const providedKeys = Object.keys(this.provideProperties());
453
463
  if (providedKeys.length > 0) {
454
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);
455
486
  // check if there are already connected elements in child dom and restart requests
456
- getAllElementChildren(this.getRoot(), BaseElement).forEach((customChild) => {
487
+ composedChildren.forEach((customChild) => {
457
488
  // if injectProperties?.() is defined it means that the child got connected BEFORE the parent (Runtime Issue)
458
489
  const requestedProperties = customChild.injectProperties?.() ?? {};
459
490
  Object.entries(requestedProperties).forEach(([key, value]) => {
460
491
  // iterate over requested properties to find missed context requests
461
- if (providedKeys.includes(key)) {
462
- // owns the provided key -> replay request
492
+ if (providedKeys.includes(key) && customChild[key] !== providedProperties[key]) {
493
+ // owns the provided key -> replay request
463
494
  customChild.requestContext(key, value);
464
495
  }
465
496
  });
@@ -90,7 +90,7 @@ class StyledElement extends BaseElement {
90
90
 
91
91
  this.getRoot().adoptedStyleSheets = [
92
92
  ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
93
- ...this.constructor['elementStyleSheets'],
93
+ ...(this.constructor?.['elementStyleSheets'] ?? []),
94
94
  ];
95
95
  }
96
96
 
@@ -107,7 +107,7 @@ class StyledElement extends BaseElement {
107
107
 
108
108
  const appendableStyles = [
109
109
  ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
110
- ...this.constructor['elementStyleSheets'],
110
+ ...(this.constructor?.['elementStyleSheets'] ?? []),
111
111
  ];
112
112
 
113
113
  appendableStyles.forEach((styleSheet, index) => {
@@ -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
@@ -54,13 +54,13 @@ class GlobalStylesStore extends Store {
54
54
  }
55
55
 
56
56
  // if it does not exist yet -> build it
57
- if (styleSheet.ownerNode.tagName === 'STYLE') {
57
+ if (styleSheet.ownerNode?.tagName === 'STYLE') {
58
58
  const cssStyleSheet = new CSSStyleSheet({ media: styleSheet.media, disabled: styleSheet.disabled });
59
59
  cssStyleSheet.replaceSync(styleSheet.ownerNode.textContent);
60
60
 
61
61
  this.globalStyleSheetsCache.set(styleSheet.ownerNode, cssStyleSheet);
62
62
  this.requestUpdate();
63
- } else if (styleSheet.ownerNode.tagName === 'LINK') {
63
+ } else if (styleSheet.ownerNode?.tagName === 'LINK') {
64
64
  const cssStyleSheet = new CSSStyleSheet({
65
65
  baseURL: styleSheet.href,
66
66
  media: styleSheet.media,