@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
@@ -0,0 +1,66 @@
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
+ if (styleSheet.ownerNode.tagName === 'STYLE') {
54
+ cssStyleSheet.replaceSync(styleSheet.ownerNode.textContent);
55
+ } else if (styleSheet.ownerNode.tagName === 'LINK') {
56
+ Array.from(styleSheet.cssRules).map((rule) => cssStyleSheet.insertRule(rule.cssText));
57
+ }
58
+ }
59
+ cssStyleSheets.push(cssStyleSheet);
60
+ });
61
+
62
+ return cssStyleSheets;
63
+ }
64
+ }
65
+
66
+ 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
+ }
package/src/util/Store.js CHANGED
@@ -1,18 +1,63 @@
1
- import { isObjectLike, deepEquals } from './AttributeParser.js';
2
- import { BaseElement } from '../BaseElement';
1
+ import { deepEquals, isObjectLike } from './AttributeParser.js';
2
+ import {
3
+ hasSerializedState,
4
+ serializeState,
5
+ deserializeState,
6
+ serializableObjectsCache,
7
+ } from './SerializeStateHelper.js';
8
+ import { BaseElement } from '../BaseElement.js';
3
9
 
10
+ /**
11
+ * Options object for the Store
12
+ * @typedef {Object} StoreOptions
13
+ * @property {string} [key] - A unique key so that the store instance can be serialized and deserialized correctly across multiple shared parents/hosts.
14
+ */
15
+
16
+ /**
17
+ * @implements {Serializable}
18
+ */
4
19
  export class Store {
5
- _observer = new Set();
20
+ _serializationKey;
21
+ _observer = new Set([]);
6
22
  _singlePropertyMode = false;
7
23
  _state = {};
8
24
 
9
- constructor(value) {
10
- // wrap primitives with a generic "value" field to generate getter and setter
25
+ /**
26
+ * @param {* | object} [value]
27
+ * @param {StoreOptions} [options]
28
+ */
29
+ constructor(value, options) {
30
+ if (options?.key && serializableObjectsCache.has(options?.key)) {
31
+ return serializableObjectsCache.get(options?.key);
32
+ }
33
+
11
34
  this._singlePropertyMode = arguments.length > 0 && !isObjectLike(value);
35
+
36
+ if (options?.key) {
37
+ this._serializationKey = options?.key;
38
+ } else {
39
+ this._serializationKey = globalThis.crypto.randomUUID();
40
+ }
41
+
42
+ if (options?.serializedState) {
43
+ deserializeState(this, options.serializedState);
44
+ } else if (hasSerializedState(this._serializationKey)) {
45
+ deserializeState(this);
46
+ }
47
+
48
+ // wrap primitives with a generic "value" field to generate getter and setter
12
49
  const specificValues = this._singlePropertyMode ? { value } : value;
13
- this._state = { ...(!this._singlePropertyMode && this.properties()), ...specificValues };
50
+ const properties = { ...(!this._singlePropertyMode && this.properties()), ...specificValues };
51
+
52
+ const serializationKeys = Object.keys(this.serializeState());
53
+ Object.entries(properties).map(([key, value]) => {
54
+ this._state[key] =
55
+ hasSerializedState(this._serializationKey) && serializationKeys.includes(key) ? this[key] : value;
56
+
57
+ if (value instanceof Store) {
58
+ value.subscribe(this);
59
+ }
14
60
 
15
- Object.entries(this._state).map(([key, value]) => {
16
61
  Object.defineProperty(this, key, {
17
62
  get: () => {
18
63
  return this._state[key];
@@ -20,6 +65,11 @@ export class Store {
20
65
  set: (newValue) => {
21
66
  const oldValue = this._state[key];
22
67
  this._state[key] = newValue;
68
+ if (value instanceof Store) {
69
+ value.subscribe(this);
70
+ }
71
+
72
+ serializeState(this);
23
73
 
24
74
  if (!deepEquals(newValue, oldValue)) {
25
75
  const watch = this.watch();
@@ -31,8 +81,15 @@ export class Store {
31
81
  },
32
82
  });
33
83
  });
84
+
85
+ serializableObjectsCache.set(this._serializationKey, this);
34
86
  }
35
87
 
88
+ /**
89
+ * Properties to be defined and assigned on the store. Can be overridden in extended classes.
90
+ * Properties defined here will also trigger an update cycle when changed.
91
+ * @return {{}}
92
+ */
36
93
  properties() {
37
94
  return {};
38
95
  }
@@ -40,47 +97,66 @@ export class Store {
40
97
  /**
41
98
  * When a property as the key was changed in the store, the callback function defined as value
42
99
  * will be called with `newValue` and `oldValue`
43
- * eg. { property: (oldValue, newValue) => { console.log('property changed!, oldValue, newValue); } }
44
100
  * @return {{}}
45
101
  */
46
102
  watch() {
47
103
  return {};
48
104
  }
49
105
 
106
+ /**
107
+ * Overrides `valueOf` to automatically return .value
108
+ * @returns {* | {}}
109
+ */
50
110
  valueOf() {
51
111
  return this._singlePropertyMode ? this._state.value : this._state;
52
112
  }
53
113
 
114
+ /**
115
+ * Overrides `toString` to automatically return .value
116
+ * @returns {string}
117
+ */
54
118
  toString() {
55
119
  return `${this._singlePropertyMode ? this._state.value : this._state}`;
56
120
  }
57
121
 
58
122
  /**
59
- * @param {BaseElement|Function} observer
123
+ * Add an observer
124
+ * @param {BaseElement|Function|Store} observer
60
125
  */
61
126
  subscribe(observer) {
62
127
  this._observer.add(observer);
63
128
  }
129
+
64
130
  /**
65
- * @param {BaseElement|Function} observer
131
+ * Remove an observer
132
+ * @param {BaseElement | Function} observer
66
133
  */
67
134
  unsubscribe(observer) {
68
135
  this._observer.delete(observer);
69
136
  }
70
137
 
138
+ /**
139
+ * Request and batch an asynchronous update cycle
140
+ */
71
141
  requestUpdate() {
72
142
  this._observer.forEach(async (observer) => {
73
- if (observer instanceof BaseElement) {
74
- await observer.requestUpdate();
143
+ if (observer instanceof BaseElement || observer instanceof Store) {
144
+ if (observer?._options?.autoUpdate !== false) {
145
+ await observer.requestUpdate();
146
+ }
75
147
  // check if store is watched by an observer
76
148
  if (Object.keys(observer.watch() ?? {}).length > 0) {
77
149
  // observer actually has watched properties
78
- const properties = observer.properties();
79
150
  Object.keys(observer.watch() ?? {}).forEach((key) => {
80
151
  if (observer._state[key] === this) {
81
152
  // observer is actually watching store changes provide new and old values
82
- observer.callPropertyWatcher(key, this, this);
83
- observer.notifyPropertyChange(key, this);
153
+ if (observer instanceof Store) {
154
+ // observer is a foreign store / call watcher
155
+ observer.watch?.()[key](this, this);
156
+ } else {
157
+ observer.callPropertyWatcher(key, this, this);
158
+ observer.notifyPropertyChange(key, this);
159
+ }
84
160
  }
85
161
  });
86
162
  }
@@ -91,4 +167,24 @@ export class Store {
91
167
  }
92
168
  });
93
169
  }
170
+
171
+ /**
172
+ * This method will be used to serialize this stores state.
173
+ * By default, it will generate an object with all properties from this.properties().
174
+ * You can override this and return a different object with more/less/different properties.
175
+ * @return {{[string: any]: *}}
176
+ */
177
+ serializeState() {
178
+ const keys = Object.keys(this.properties());
179
+ return Object.fromEntries(keys.map((key) => [key, this[key]]));
180
+ }
181
+
182
+ /**
183
+ * This method will be used to restore properties on the store from a previously serialized state.
184
+ * You can override this and handle the deserialization differently if the default is not enough.
185
+ * @param {{[string: any]: *}} serializedState
186
+ */
187
+ restoreState(serializedState) {
188
+ Object.assign(this, serializedState);
189
+ }
94
190
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Wrapper for defining custom elements so that registering an element multiple times won't crash
3
- * @param name for the tag
4
- * @param constructor for the custom element
3
+ * @param {string} name - name for the element tag
4
+ * @param {CustomElementConstructor} constructor for the custom element
5
5
  */
6
6
  export function defineElement(name, constructor) {
7
7
  try {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Little wrapper function for JSON.stringify() to easily convert objects and arrays to strings
3
3
  * to be able to set them as attributes on custom elements
4
- * @param value to be stringified
5
- * @return String of the stringified value
4
+ * @param {object | array} value - to be stringified
5
+ * @return {string}
6
6
  */
7
7
  export function toString(value) {
8
8
  try {