@webtides/element-js 1.0.0-alpha.2 → 1.0.0-alpha.3

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/index.js CHANGED
@@ -3,8 +3,14 @@ import { StyledElement } from './src/StyledElement.js';
3
3
  import { TemplateElement, html } from './src/TemplateElement.js';
4
4
  import { toString } from './src/util/toString.js';
5
5
  import { defineElement } from './src/util/defineElement.js';
6
- import { i18n } from './src/util/i18n.js';
7
6
  import { Store } from './src/util/Store.js';
8
7
  import { Directive } from './src/util/Directive.js';
9
8
 
10
- export { BaseElement, StyledElement, TemplateElement, Store, Directive, html, toString, defineElement, i18n };
9
+ /**
10
+ * Options object for element-js
11
+ * @typedef {Object} ElementJsConfig
12
+ * @property {boolean} [serializeState] - When set to true all elements and stores will serialize their state on changes to a global JSON object in the document. This is needed when rendering server side, so that elements and stores can hydrate with the correct state.
13
+ * @property {boolean} [observeGlobalStyles] - By default element-js will look for all style elements in the global document and apply them before any custom/element styles inside the shadow DOM. When set to true element-js will also observe the document for any changes regarding styles. This is needed if styles will get added async or late.
14
+ */
15
+
16
+ export { BaseElement, StyledElement, TemplateElement, Store, Directive, html, toString, defineElement };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.3",
4
4
  "module": "index.js",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
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
 
@@ -39,7 +39,12 @@ export { toString } from './util/toString.js';
39
39
  * @property {any} oldValue - The old value for the property
40
40
  */
41
41
 
42
+ /**
43
+ * @implements {Serializable}
44
+ */
42
45
  class BaseElement extends HTMLElement {
46
+ _serializationKey;
47
+
43
48
  /**
44
49
  * @param {BaseElementOptions} options
45
50
  */
@@ -54,6 +59,7 @@ class BaseElement extends HTMLElement {
54
59
  this._options = {
55
60
  autoUpdate: true,
56
61
  deferUpdate: true,
62
+ deferConnected: false,
57
63
  mutationObserverOptions: {
58
64
  attributes: true,
59
65
  childList: true,
@@ -63,6 +69,13 @@ class BaseElement extends HTMLElement {
63
69
  propertyOptions: {},
64
70
  ...options,
65
71
  };
72
+
73
+ if (this.hasAttribute('defer-update')) {
74
+ this._options.deferUpdate = true;
75
+ }
76
+ if (this.hasAttribute('defer-connected')) {
77
+ this._options.deferConnected = true;
78
+ }
66
79
  }
67
80
 
68
81
  /**
@@ -71,6 +84,21 @@ class BaseElement extends HTMLElement {
71
84
  * register observers, refs and events.
72
85
  */
73
86
  connectedCallback() {
87
+ if (this._options.deferConnected) {
88
+ this._options.deferConnected = false;
89
+ return;
90
+ }
91
+
92
+ if (this.hasAttribute('ejs:key')) {
93
+ this._serializationKey = this.getAttribute('ejs:key');
94
+ deserializeState(this);
95
+ } else {
96
+ this._serializationKey = globalThis.crypto.randomUUID();
97
+ if (globalThis.elementJsConfig?.serializeState) {
98
+ this.setAttribute('ejs:key', this._serializationKey);
99
+ }
100
+ }
101
+
74
102
  // define all attributes to "this" as properties
75
103
  this.defineAttributesAsProperties();
76
104
 
@@ -261,10 +289,6 @@ class BaseElement extends HTMLElement {
261
289
  * @param {boolean} reflectAttribute
262
290
  */
263
291
  defineProperty(property, value, reflectAttribute = false) {
264
- if (value instanceof Store) {
265
- value.subscribe(this);
266
- }
267
-
268
292
  if (this._state.hasOwnProperty(property)) {
269
293
  // property has already been defined as an attribute nothing to do here
270
294
  return;
@@ -280,8 +304,16 @@ class BaseElement extends HTMLElement {
280
304
  this.removeAttribute(camelToDash(property));
281
305
  }
282
306
 
307
+ if (hasSerializedState(this._serializationKey) && Object.keys(this.serializeState())?.includes(property)) {
308
+ value = this[property];
309
+ }
310
+
283
311
  this._state[property] = value;
284
312
 
313
+ if (this._state[property] instanceof Store) {
314
+ this._state[property].subscribe(this);
315
+ }
316
+
285
317
  Object.defineProperty(this, property, {
286
318
  get: () => {
287
319
  return this._state[property];
@@ -292,6 +324,8 @@ class BaseElement extends HTMLElement {
292
324
  if (JSON.stringify(oldValue) !== newValueString) {
293
325
  this._state[property] = newValue;
294
326
 
327
+ serializeState(this);
328
+
295
329
  if (newValue instanceof Store) {
296
330
  newValue.subscribe(this);
297
331
  }
@@ -395,8 +429,21 @@ class BaseElement extends HTMLElement {
395
429
  this.requestContext(key, value);
396
430
  });
397
431
  // define context provider
398
- if (Object.keys(this.provideProperties()).length > 0) {
432
+ const providedKeys = Object.keys(this.provideProperties());
433
+ if (providedKeys.length > 0) {
399
434
  this.addEventListener('request-context', this.onRequestContext);
435
+ // check if there are already connected elements in child dom and restart requests
436
+ getAllElementChildren(this.getRoot(), BaseElement).forEach((customChild) => {
437
+ // if injectProperties?.() is defined it means that the child got connected BEFORE the parent (Runtime Issue)
438
+ const requestedProperties = customChild.injectProperties?.() ?? {};
439
+ Object.entries(requestedProperties).forEach(([key, value]) => {
440
+ // iterate over requested properties to find missed context requests
441
+ if (providedKeys.includes(key)) {
442
+ // owns the provided key -> replay request
443
+ customChild.requestContext(key, value);
444
+ }
445
+ });
446
+ });
400
447
  }
401
448
  }
402
449
 
@@ -406,7 +453,7 @@ class BaseElement extends HTMLElement {
406
453
  * @param {any | function} valueOrCallback
407
454
  */
408
455
  requestContext(propertyName, valueOrCallback) {
409
- this.dispatch('request-context', { [propertyName]: valueOrCallback }, true);
456
+ this.dispatch('request-context', { [propertyName]: valueOrCallback }, true, true, true);
410
457
  }
411
458
 
412
459
  /**
@@ -416,7 +463,6 @@ class BaseElement extends HTMLElement {
416
463
  * @param {CustomEvent} event
417
464
  */
418
465
  onRequestContext(event) {
419
- const properties = this.properties();
420
466
  const provideProperties = this.provideProperties();
421
467
 
422
468
  Object.entries(event.detail ?? {}).forEach(([key, valueOrCallback]) => {
@@ -429,7 +475,9 @@ class BaseElement extends HTMLElement {
429
475
  valueOrCallback(providedValue);
430
476
  } else {
431
477
  // assign to prop
432
- event.target[key] = providedValue;
478
+ const eventPath = event.composedPath();
479
+ const target = eventPath[0] || event.target;
480
+ target[key] = providedValue;
433
481
  }
434
482
  }
435
483
  });
@@ -572,17 +620,32 @@ class BaseElement extends HTMLElement {
572
620
  * Helper function for dispatching custom events
573
621
  * @param {string} name
574
622
  * @param {Object} data
575
- * @param {boolean} bubble
576
- * @param {boolean} cancelable
577
- * @param {boolean} composed
578
- */
579
- dispatch(name, data, bubble = false, cancelable = false, composed = false) {
580
- const event = new CustomEvent(name, {
581
- bubbles: bubble,
582
- cancelable: cancelable,
583
- composed: composed,
584
- detail: data,
585
- });
623
+ * @param {CustomEventInit} options
624
+ * @param {boolean} cancelable - DEPRECATED
625
+ * @param {boolean} composed - DEPRECATED
626
+ */
627
+ dispatch(name, data, options, cancelable = false, composed = false) {
628
+ let eventInit = {};
629
+
630
+ if (options !== undefined && typeof options === 'boolean') {
631
+ eventInit['bubbles'] = options;
632
+ eventInit['cancelable'] = cancelable;
633
+ eventInit['composed'] = composed;
634
+ eventInit['detail'] = data;
635
+ console.warn(
636
+ `[${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`,
637
+ );
638
+ } else {
639
+ eventInit = {
640
+ bubbles: true,
641
+ cancelable: true,
642
+ composed: true,
643
+ detail: data,
644
+ ...options,
645
+ };
646
+ }
647
+
648
+ const event = new CustomEvent(name, eventInit);
586
649
  this.dispatchEvent(event);
587
650
  }
588
651
 
@@ -593,6 +656,26 @@ class BaseElement extends HTMLElement {
593
656
  getRoot() {
594
657
  return this;
595
658
  }
659
+
660
+ /**
661
+ * This method will be used to serialize this elements state.
662
+ * By default, it will generate an object with all properties from this.properties().
663
+ * You can override this and return a different object with more/less/different properties.
664
+ * @return {{[string: any]: *}}
665
+ */
666
+ serializeState() {
667
+ const keys = Object.keys(this.properties());
668
+ return Object.fromEntries(keys.map((key) => [key, this[key]]));
669
+ }
670
+
671
+ /**
672
+ * This method will be used to restore properties on the element from a previously serialized state.
673
+ * You can override this and handle the deserialization differently if the default is not enough.
674
+ * @param {{[string: any]: *}} serializedState
675
+ */
676
+ restoreState(serializedState) {
677
+ Object.assign(this, serializedState);
678
+ }
596
679
  }
597
680
 
598
681
  export { BaseElement, getClosestParentCustomElementNode, isOfSameNodeType };
@@ -1,6 +1,6 @@
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
5
  /**
6
6
  * Options object for the StyledElement
@@ -8,23 +8,10 @@ export { i18n } from './util/i18n.js';
8
8
  * @extends BaseElementOptions
9
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
10
  * @property {[]} [styles] - Via the styles option you can add multiple styles/stylesheets to your element. Default is `[]`
11
- * @property {boolean} [adoptGlobalStyles] - When set to true element-js will look for a style element in the global document with an id of "globalStyles" and apply it before any custom/element styles inside the shadow DOM. Default is `true`
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
12
  */
13
13
 
14
14
  class StyledElement extends BaseElement {
15
- static globalStyles = null;
16
-
17
- static updateGlobalStyles() {
18
- // this is a runtime dependency so that every shadow dom can make use of global css
19
- // we assume these styles to be inlined into the document
20
- StyledElement.globalStyles = globalThis.document?.getElementById('globalStyles');
21
-
22
- if (StyledElement.globalStyles && StyledElement['globalStyleSheet']) {
23
- //updates already adopted global styles
24
- StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
25
- }
26
- }
27
-
28
15
  /**
29
16
  * @param {StyledElementOptions} options
30
17
  */
@@ -45,9 +32,23 @@ class StyledElement extends BaseElement {
45
32
  connectedCallback() {
46
33
  super.connectedCallback();
47
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
+
48
43
  if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
49
44
  // adopting does only make sense in shadow dom. Fall back to append for light elements
50
45
  this.adoptStyleSheets();
46
+
47
+ if (this._options.adoptGlobalStyles !== false) {
48
+ globalStylesStore.subscribe(() => {
49
+ this.adoptStyleSheets();
50
+ });
51
+ }
51
52
  }
52
53
  }
53
54
 
@@ -65,13 +66,9 @@ class StyledElement extends BaseElement {
65
66
  * @param {PropertyUpdateOptions} options
66
67
  */
67
68
  update(options) {
69
+ // We cannot do this in connectedCallback() since the whole template will be overridden in update/render afterward
68
70
  if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
69
- // append stylesheets to template if not already adopted
70
- const appendableStyles = [...this._styles];
71
- if (this._options.shadowRender && this._options.adoptGlobalStyles) {
72
- appendableStyles.unshift(StyledElement.globalStyles?.textContent ?? '');
73
- }
74
- this.appendStyleSheets(appendableStyles);
71
+ this.appendStyleSheets();
75
72
  }
76
73
  super.update(options);
77
74
  }
@@ -80,39 +77,29 @@ class StyledElement extends BaseElement {
80
77
  * Adopt stylesheets
81
78
  */
82
79
  adoptStyleSheets() {
83
- if (!this.constructor['cssStyleSheets']) {
84
- this.constructor['cssStyleSheets'] = this._styles.map((style) => {
85
- const sheet = new CSSStyleSheet();
86
- if (sheet && sheet.cssRules.length === 0) {
87
- sheet.replaceSync(style);
88
- }
89
- return sheet;
90
- });
91
- }
92
-
93
- if (StyledElement.globalStyles && !StyledElement['globalStyleSheet']) {
94
- StyledElement['globalStyleSheet'] = new CSSStyleSheet();
95
- StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
96
- }
80
+ const adoptGlobalStyleSheets = this._options.shadowRender && this._options.adoptGlobalStyles !== false;
97
81
 
98
- // adopt styles
99
- // uses proposed solution for constructable stylesheets
100
- // see: https://wicg.github.io/construct-stylesheets/#proposed-solution
101
82
  this.getRoot().adoptedStyleSheets = [
102
- ...(this._options.shadowRender && this._options.adoptGlobalStyles && StyledElement['globalStyleSheet']
103
- ? [StyledElement['globalStyleSheet']]
104
- : []),
105
- ...this.constructor['cssStyleSheets'],
83
+ ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
84
+ ...this.constructor['elementStyleSheets'],
106
85
  ];
107
86
  }
108
87
 
109
88
  /**
110
89
  * Custom polyfill for constructable stylesheets by appending styles to the end of an element
111
- * @param {string[]} styles
112
90
  */
113
- appendStyleSheets(styles) {
91
+ appendStyleSheets() {
114
92
  const parentDocument = getShadowParentOrBody(this.getRoot());
115
- styles.forEach((style, index) => {
93
+
94
+ const adoptGlobalStyleSheets =
95
+ this._options.shadowRender && this._options.adoptGlobalStyles !== false && parentDocument !== document.body;
96
+
97
+ const appendableStyles = [
98
+ ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
99
+ ...this.constructor['elementStyleSheets'],
100
+ ];
101
+
102
+ appendableStyles.forEach((styleSheet, index) => {
116
103
  const identifier = this.tagName + index;
117
104
 
118
105
  // only append stylesheet if not already appended to shadowRoot or document
@@ -120,13 +107,13 @@ class StyledElement extends BaseElement {
120
107
  const styleElement = globalThis.document?.createElement('style');
121
108
  styleElement.id = identifier;
122
109
  styleElement.style.display = 'none';
123
- styleElement.textContent = style;
110
+ styleElement.textContent = Array.from(styleSheet.cssRules)
111
+ .map((rule) => rule.cssText)
112
+ .join('');
124
113
  parentDocument.appendChild(styleElement);
125
114
  }
126
115
  });
127
116
  }
128
117
  }
129
118
 
130
- StyledElement.updateGlobalStyles();
131
-
132
119
  export { StyledElement };
@@ -1,7 +1,6 @@
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';
5
4
 
6
5
  /**
7
6
  * Options object for the TemplateElement
@@ -75,6 +75,26 @@ export function getClosestParentCustomElementNode(domNode) {
75
75
  return customElementParents.pop();
76
76
  }
77
77
 
78
+ /**
79
+ * Get all
80
+ * @param {Node} domNode
81
+ * @param {{prototype: HTMLElement, new()}} instanceOfFilter
82
+ * @return {Node[]}
83
+ */
84
+ export function getAllElementChildren(domNode, instanceOfFilter = HTMLElement) {
85
+ const nodeIterator = document.createNodeIterator(domNode, NodeFilter.SHOW_ELEMENT, (node) =>
86
+ node.nodeName.includes('-') && node instanceof instanceOfFilter
87
+ ? NodeFilter.FILTER_ACCEPT
88
+ : NodeFilter.FILTER_REJECT,
89
+ );
90
+ const customElements = [];
91
+ let currentNode;
92
+ while ((currentNode = nodeIterator.nextNode())) {
93
+ customElements.push(currentNode);
94
+ }
95
+ return customElements;
96
+ }
97
+
78
98
  /**
79
99
  * Checks whether two nodes are of the same node type
80
100
  * @param {Node} nodeA
@@ -0,0 +1,70 @@
1
+ import { Store } from './Store.js';
2
+
3
+ /**
4
+ * @property {CSSStyleSheet[]} cssStyleSheets
5
+ */
6
+ class GlobalStylesStore extends Store {
7
+ /** @type {Map<Node, CSSStyleSheet>} */
8
+ globalStyleSheetsCache = new WeakMap();
9
+
10
+ constructor() {
11
+ super();
12
+ if (globalThis.elementJsConfig?.observeGlobalStyles) {
13
+ const mutationObserver = new MutationObserver((mutationRecord) => {
14
+ if (!mutationRecord[0]) return;
15
+ const filteredNodes = Array.from(mutationRecord[0].addedNodes).filter(
16
+ (node) => node.tagName === 'STYLE' || node.tagName === 'LINK',
17
+ );
18
+ if (filteredNodes && filteredNodes[0] && filteredNodes[0].tagName === 'LINK') {
19
+ filteredNodes[0].onload = () => {
20
+ this.requestUpdate();
21
+ };
22
+ } else if (filteredNodes && filteredNodes[0] && filteredNodes[0].tagName === 'STYLE') {
23
+ this.requestUpdate();
24
+ }
25
+ });
26
+ mutationObserver.observe(globalThis.document, { subtree: true, childList: true });
27
+ }
28
+ }
29
+
30
+ getGlobalStyleSheets(selector) {
31
+ /** @type {CSSStyleSheet[]}*/
32
+ const cssStyleSheets = [];
33
+
34
+ if (selector === false) return cssStyleSheets;
35
+
36
+ if (typeof selector === 'string') {
37
+ selector = [selector];
38
+ }
39
+
40
+ if (selector === true || selector.includes('document')) {
41
+ cssStyleSheets.push(...globalThis.document?.adoptedStyleSheets);
42
+ }
43
+
44
+ Array.from(globalThis.document?.styleSheets).map((styleSheet) => {
45
+ if (Array.isArray(selector) && !selector.some((cssSelector) => styleSheet.ownerNode.matches(cssSelector))) {
46
+ return;
47
+ }
48
+
49
+ // TODO: this will always be null as we never set anything to the cache...
50
+ let cssStyleSheet = this.globalStyleSheetsCache.get(styleSheet.ownerNode);
51
+ if (!cssStyleSheet) {
52
+ cssStyleSheet = new CSSStyleSheet();
53
+ let cssText = '';
54
+ if (styleSheet.ownerNode.tagName === 'STYLE') {
55
+ cssText = styleSheet.ownerNode.textContent;
56
+ } else if (styleSheet.ownerNode.tagName === 'LINK') {
57
+ cssText = Array.from(styleSheet.cssRules)
58
+ .map((rule) => rule.cssText)
59
+ .join('');
60
+ }
61
+ cssStyleSheet.replaceSync(cssText);
62
+ }
63
+ cssStyleSheets.push(cssStyleSheet);
64
+ });
65
+
66
+ return cssStyleSheets;
67
+ }
68
+ }
69
+
70
+ export const globalStylesStore = new GlobalStylesStore();
@@ -0,0 +1,103 @@
1
+ import { Store } from './Store.js';
2
+
3
+ /**
4
+ * @typedef {Object} Serializable
5
+ * An interface that classes should implement to enable serialization and deserialization of their state.
6
+ * @property {string} _serializationKey - a unique key to be used for serialization.
7
+ * @property {function} serializeState - Function to retrieve the state for serialization.
8
+ * @property {function} restoreState - Function to set the state during deserialization.
9
+ */
10
+
11
+ // TODO: is it ok to expose this like this? Or should we wrap the cache in helper methods also?
12
+ /** @type {Map<string, Serializable>} */
13
+ export const serializableObjectsCache = new Map();
14
+
15
+ /** @type {HTMLScriptElement} */
16
+ let globalElementJsState;
17
+
18
+ /**
19
+ * Initializes the global script element that holds the state for all `Serializable` objects.
20
+ */
21
+ function initGlobalStateObject() {
22
+ if (!globalElementJsState) {
23
+ globalElementJsState = Array.from(globalThis.document.scripts).find((script) => script.type === 'ejs/json');
24
+ if (!globalElementJsState) {
25
+ const script = document.createElement('script');
26
+ script.setAttribute('type', 'ejs/json');
27
+ script.textContent = '{}';
28
+ document.body.appendChild(script);
29
+ globalElementJsState = script;
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Takes an object that implements the `Serializable` interface and serializes its state.
36
+ * @param {Serializable} serializableObject
37
+ */
38
+ export function serializeState(serializableObject) {
39
+ if (!globalThis.elementJsConfig?.serializeState) return;
40
+
41
+ if (!serializableObject._serializationKey && !serializableObject.serializeState) {
42
+ throw new Error('serializableObject does not implement the Serializable interface');
43
+ }
44
+
45
+ initGlobalStateObject();
46
+
47
+ const currentState = JSON.parse(globalElementJsState.textContent);
48
+ currentState[serializableObject._serializationKey] = serializableObject.serializeState();
49
+ globalElementJsState.textContent = JSON.stringify(currentState, (key, value) => {
50
+ if (value instanceof Store) {
51
+ return 'Store/' + value._serializationKey;
52
+ } else {
53
+ return value;
54
+ }
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Takes an object that implements the `Serializable` interface and deserializes its state and restores the object.
60
+ * @param {Serializable} serializableObject
61
+ * @param {{[string: any]: *}} [serializedState]
62
+ */
63
+ export function deserializeState(serializableObject, serializedState) {
64
+ if (!globalThis.elementJsConfig?.serializeState) return;
65
+
66
+ if (!serializableObject._serializationKey && !serializableObject.restoreState) {
67
+ throw new Error('serializableObject does not implement the Serializable interface');
68
+ }
69
+
70
+ if (serializedState) {
71
+ serializableObject.restoreState(serializedState);
72
+ return;
73
+ }
74
+
75
+ initGlobalStateObject();
76
+
77
+ const unresolvedState = JSON.parse(globalElementJsState.textContent);
78
+ const currentState = JSON.parse(globalElementJsState.textContent, (key, value) => {
79
+ if (typeof value === 'string' && value.startsWith('Store/')) {
80
+ const [_, storeUuid] = value.split('/');
81
+ const serializedState = unresolvedState[storeUuid];
82
+ return new Store(serializedState, { key: storeUuid, serializedState });
83
+ } else {
84
+ return value;
85
+ }
86
+ });
87
+
88
+ serializableObject.restoreState(currentState[serializableObject._serializationKey]);
89
+ }
90
+
91
+ /**
92
+ * Checks if a serialized state is available for a given key.
93
+ * @param key
94
+ * @return {boolean}
95
+ */
96
+ export function hasSerializedState(key) {
97
+ if (!globalThis.elementJsConfig?.serializeState) return false;
98
+
99
+ initGlobalStateObject();
100
+
101
+ const serializedState = JSON.parse(globalElementJsState.textContent);
102
+ return serializedState.hasOwnProperty(key);
103
+ }