@webtides/element-js 1.0.0-alpha.9 → 1.1.0

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 (41) hide show
  1. package/CHANGELOG.md +369 -0
  2. package/README.md +3 -3
  3. package/docs/README.md +70 -89
  4. package/docs/concepts/classes.md +37 -66
  5. package/docs/concepts/hierarchy.md +29 -31
  6. package/docs/concepts/lifecycle.md +9 -23
  7. package/docs/concepts/reactivity.md +1 -3
  8. package/docs/features/computed-properties.md +13 -16
  9. package/docs/features/dependency-injection-context-protocol.md +75 -87
  10. package/docs/features/global-styles.md +1 -1
  11. package/docs/features/methods.md +14 -15
  12. package/docs/features/properties.md +21 -33
  13. package/docs/features/refs.md +23 -31
  14. package/docs/features/renderless.md +31 -37
  15. package/docs/features/store.md +63 -78
  16. package/docs/features/templates.md +4 -1
  17. package/docs/features/watch.md +28 -35
  18. package/docs/getting-started/upgrade-to-v1.md +16 -0
  19. package/docs/guide/guide.md +10 -22
  20. package/index.js +29 -2
  21. package/package.json +62 -62
  22. package/src/BaseElement.js +637 -632
  23. package/src/StyledElement.js +104 -104
  24. package/src/TemplateElement.js +53 -58
  25. package/src/dom-parts/AttributePart.js +116 -116
  26. package/src/dom-parts/ChildNodePart.js +288 -288
  27. package/src/dom-parts/NodePart.js +27 -27
  28. package/src/dom-parts/Part.js +6 -6
  29. package/src/dom-parts/TemplatePart.js +119 -119
  30. package/src/dom-parts/TemplateResult.js +274 -267
  31. package/src/dom-parts/directives.js +105 -48
  32. package/src/dom-parts/html.js +2 -2
  33. package/src/dom-parts/render.js +6 -1
  34. package/src/util/AttributeParser.js +47 -47
  35. package/src/util/DOMHelper.js +190 -191
  36. package/src/util/Directive.js +31 -24
  37. package/src/util/GlobalStylesStore.js +56 -49
  38. package/src/util/SerializeStateHelper.js +56 -56
  39. package/src/util/Store.js +174 -174
  40. package/src/util/defineElement.js +5 -5
  41. package/src/util/toString.js +5 -5
@@ -8,36 +8,36 @@ import { TemplatePart } from './TemplatePart.js';
8
8
  * @return {ChildNode}
9
9
  */
10
10
  const removeChildNodes = (node) => {
11
- const range = globalThis.document?.createRange();
12
- const firstChild = Array.isArray(node)
13
- ? node[0]
14
- : node instanceof TemplatePart
15
- ? node.childNodes[0]
16
- : node.firstChild;
17
- const lastChild = Array.isArray(node)
18
- ? node[node.length - 1]
19
- : node instanceof TemplatePart
20
- ? node.childNodes[node.childNodes.length - 1]
21
- : node.lastChild;
22
- range.setStartAfter(firstChild);
23
- range.setEndAfter(lastChild);
24
- range.deleteContents();
25
- return firstChild;
11
+ const range = globalThis.document?.createRange();
12
+ const firstChild = Array.isArray(node)
13
+ ? node[0]
14
+ : node instanceof TemplatePart
15
+ ? node.childNodes[0]
16
+ : node.firstChild;
17
+ const lastChild = Array.isArray(node)
18
+ ? node[node.length - 1]
19
+ : node instanceof TemplatePart
20
+ ? node.childNodes[node.childNodes.length - 1]
21
+ : node.lastChild;
22
+ range.setStartAfter(firstChild);
23
+ range.setEndAfter(lastChild);
24
+ range.deleteContents();
25
+ return firstChild;
26
26
  };
27
27
 
28
28
  /**
29
29
  * @param {Element} commentNode
30
30
  */
31
31
  const removeNodesBetweenComments = (commentNode) => {
32
- while (commentNode.previousSibling) {
33
- if (
34
- commentNode.previousSibling.nodeType === COMMENT_NODE &&
35
- commentNode.previousSibling.data === commentNode.data.replace('/', '')
36
- ) {
37
- break;
38
- }
39
- commentNode.parentNode.removeChild(commentNode.previousSibling);
40
- }
32
+ while (commentNode.previousSibling) {
33
+ if (
34
+ commentNode.previousSibling.nodeType === COMMENT_NODE &&
35
+ commentNode.previousSibling.data === commentNode.data.replace('/', '')
36
+ ) {
37
+ break;
38
+ }
39
+ commentNode.parentNode.removeChild(commentNode.previousSibling);
40
+ }
41
41
  };
42
42
 
43
43
  /**
@@ -48,41 +48,41 @@ const removeNodesBetweenComments = (commentNode) => {
48
48
  * @return {Node[]}
49
49
  */
50
50
  const diffChildNodes = function (parentNode, domChildNodes, templateChildNodes, anchorNode) {
51
- // Diff each node in the child node lists
52
- let length = Math.max(templateChildNodes.length, domChildNodes.length);
53
- for (let index = 0; index < length; index++) {
54
- let domChildNode = domChildNodes[index];
55
- let templateChildNode = templateChildNodes[index];
56
-
57
- // If the DOM node doesn't exist, append/copy the template node
58
- if (!domChildNode) {
59
- if (templateChildNode instanceof TemplatePart) {
60
- anchorNode.before(...templateChildNode.childNodes);
61
- } else if (typeof templateChildNode === 'object' && 'ELEMENT_NODE' in templateChildNode) {
62
- parentNode.insertBefore(templateChildNode, anchorNode);
63
- } else {
64
- // TODO: this might not be performant?! Can we maybe handle this in processDomNode?!
65
- // TODO: I think that we need to make ChildNodeParts from all primitive values as well
66
- parentNode.insertBefore(document.createTextNode(templateChildNode), anchorNode);
67
- }
68
- continue;
69
- }
70
-
71
- // If the template node doesn't exist, remove the node in the DOM
72
- if (!templateChildNode) {
73
- parentNode.removeChild(removeChildNodes(domChildNode));
74
- continue;
75
- }
76
-
77
- // If DOM node is equal to the template node, don't do anything
78
- if (domChildNode === templateChildNode) {
79
- continue;
80
- }
81
-
82
- // Everything else is somehow different and can be replaced
83
- parentNode.replaceChild(templateChildNode, domChildNode);
84
- }
85
- return templateChildNodes;
51
+ // Diff each node in the child node lists
52
+ let length = Math.max(templateChildNodes.length, domChildNodes.length);
53
+ for (let index = 0; index < length; index++) {
54
+ let domChildNode = domChildNodes[index];
55
+ let templateChildNode = templateChildNodes[index];
56
+
57
+ // If the DOM node doesn't exist, append/copy the template node
58
+ if (!domChildNode) {
59
+ if (templateChildNode instanceof TemplatePart) {
60
+ anchorNode.before(...templateChildNode.childNodes);
61
+ } else if (typeof templateChildNode === 'object' && 'ELEMENT_NODE' in templateChildNode) {
62
+ parentNode.insertBefore(templateChildNode, anchorNode);
63
+ } else {
64
+ // TODO: this might not be performant?! Can we maybe handle this in processDomNode?!
65
+ // TODO: I think that we need to make ChildNodeParts from all primitive values as well
66
+ parentNode.insertBefore(document.createTextNode(templateChildNode), anchorNode);
67
+ }
68
+ continue;
69
+ }
70
+
71
+ // If the template node doesn't exist, remove the node in the DOM
72
+ if (!templateChildNode) {
73
+ parentNode.removeChild(removeChildNodes(domChildNode));
74
+ continue;
75
+ }
76
+
77
+ // If DOM node is equal to the template node, don't do anything
78
+ if (domChildNode === templateChildNode) {
79
+ continue;
80
+ }
81
+
82
+ // Everything else is somehow different and can be replaced
83
+ parentNode.replaceChild(templateChildNode, domChildNode);
84
+ }
85
+ return templateChildNodes;
86
86
  };
87
87
 
88
88
  /**
@@ -91,239 +91,239 @@ const diffChildNodes = function (parentNode, domChildNodes, templateChildNodes,
91
91
  * @return {(function(*): void)|*}
92
92
  */
93
93
  export const processNodePart = (comment, initialValue) => {
94
- let nodes = [];
95
- let oldValue = initialValue instanceof TemplatePart ? [...initialValue.childNodes] : initialValue;
96
- // this is for string values to be inserted into the DOM. A cached TextNode will be used so that we don't have to constantly create new DOM nodes.
97
- let cachedTextNode = undefined;
98
-
99
- const processNodeValue = (newValue) => {
100
- switch (typeof newValue) {
101
- // primitives are handled as text content
102
- case 'string':
103
- case 'number':
104
- case 'boolean':
105
- if (oldValue !== newValue) {
106
- oldValue = newValue;
107
- if (!cachedTextNode) cachedTextNode = globalThis.document?.createTextNode('');
108
- cachedTextNode.data = newValue;
109
-
110
- if (comment.previousSibling?.data === comment.data.replace('/', '')) {
111
- // the part is empty - we haven't rendered it yet
112
- comment.parentNode.insertBefore(cachedTextNode, comment);
113
- } else {
114
- // the part was already rendered (either server side or on the client)
115
- comment.previousSibling.data = newValue;
116
- }
117
-
118
- nodes = [cachedTextNode];
119
- }
120
- break;
121
- // null (= typeof "object") and undefined are used to clean up previous content
122
- case 'object':
123
- case 'undefined':
124
- if (newValue === null || newValue === undefined) {
125
- if (oldValue !== newValue) {
126
- oldValue = newValue;
127
- removeNodesBetweenComments(comment);
128
- nodes = [];
129
- }
130
- break;
131
- }
132
- if (Array.isArray(newValue)) {
133
- if (newValue.length === 0) {
134
- removeNodesBetweenComments(comment);
135
- nodes = [];
136
- } else {
137
- nodes = diffChildNodes(comment.parentNode, oldValue || [], newValue, comment);
138
- }
139
- oldValue = newValue;
140
- break;
141
- }
142
- // if the new value is a node or a fragment, and it's different from the live node, then it's diffed.
143
- if (oldValue !== newValue) {
144
- if ('ELEMENT_NODE' in newValue) {
145
- oldValue = newValue;
146
- nodes = diffChildNodes(comment.parentNode, nodes, [newValue], comment);
147
- } else if (newValue instanceof TemplatePart) {
148
- oldValue = newValue;
149
- nodes = diffChildNodes(comment.parentNode, nodes, [...newValue.childNodes], comment);
150
- } else {
151
- console.warn('Could not process value', newValue);
152
- }
153
- }
154
- break;
155
- case 'function':
156
- processNodeValue(newValue(comment));
157
- break;
158
- }
159
- };
160
-
161
- return processNodeValue;
94
+ let nodes = [];
95
+ let oldValue = initialValue instanceof TemplatePart ? [...initialValue.childNodes] : initialValue;
96
+ // this is for string values to be inserted into the DOM. A cached TextNode will be used so that we don't have to constantly create new DOM nodes.
97
+ let cachedTextNode = undefined;
98
+
99
+ const processNodeValue = (newValue) => {
100
+ switch (typeof newValue) {
101
+ // primitives are handled as text content
102
+ case 'string':
103
+ case 'number':
104
+ case 'boolean':
105
+ if (oldValue !== newValue) {
106
+ oldValue = newValue;
107
+ if (!cachedTextNode) cachedTextNode = globalThis.document?.createTextNode('');
108
+ cachedTextNode.data = newValue;
109
+
110
+ if (comment.previousSibling?.data === comment.data.replace('/', '')) {
111
+ // the part is empty - we haven't rendered it yet
112
+ comment.parentNode.insertBefore(cachedTextNode, comment);
113
+ } else {
114
+ // the part was already rendered (either server side or on the client)
115
+ comment.previousSibling.data = newValue;
116
+ }
117
+
118
+ nodes = [cachedTextNode];
119
+ }
120
+ break;
121
+ // null (= typeof "object") and undefined are used to clean up previous content
122
+ case 'object':
123
+ case 'undefined':
124
+ if (newValue === null || newValue === undefined) {
125
+ if (oldValue !== newValue) {
126
+ oldValue = newValue;
127
+ removeNodesBetweenComments(comment);
128
+ nodes = [];
129
+ }
130
+ break;
131
+ }
132
+ if (Array.isArray(newValue)) {
133
+ if (newValue.length === 0) {
134
+ removeNodesBetweenComments(comment);
135
+ nodes = [];
136
+ } else {
137
+ nodes = diffChildNodes(comment.parentNode, oldValue || [], newValue, comment);
138
+ }
139
+ oldValue = newValue;
140
+ break;
141
+ }
142
+ // if the new value is a node or a fragment, and it's different from the live node, then it's diffed.
143
+ if (oldValue !== newValue) {
144
+ if ('ELEMENT_NODE' in newValue) {
145
+ oldValue = newValue;
146
+ nodes = diffChildNodes(comment.parentNode, nodes, [newValue], comment);
147
+ } else if (newValue instanceof TemplatePart) {
148
+ oldValue = newValue;
149
+ nodes = diffChildNodes(comment.parentNode, nodes, [...newValue.childNodes], comment);
150
+ } else {
151
+ console.warn('Could not process value', newValue);
152
+ }
153
+ }
154
+ break;
155
+ case 'function':
156
+ processNodeValue(newValue(comment));
157
+ break;
158
+ }
159
+ };
160
+
161
+ return processNodeValue;
162
162
  };
163
163
 
164
164
  /**
165
165
  * For updating a text node, a sequence of child nodes or an array of `any` values
166
166
  */
167
167
  export class ChildNodePart extends Part {
168
- /** @type {Node | undefined} */
169
- startNode = undefined;
170
-
171
- /** @type {Node | undefined} */
172
- endNode = undefined;
173
-
174
- // TODO: this is only for array values
175
- /** @type {Part[]} */
176
- parts = [];
177
-
178
- // TODO: this is only for TemplatePart values
179
- templatePart = undefined;
180
-
181
- // TODO: maybe we can store a generic vale|parsedValue that can be either parts[] | templatePart | any
182
-
183
- /** @type {Node[]} */
184
- childNodes = [];
185
-
186
- /**
187
- * @param {Node} startNode - the start comment marker node
188
- * @param {any | any[]} value
189
- */
190
- constructor(startNode, value) {
191
- if (startNode && startNode?.nodeType !== COMMENT_NODE) {
192
- throw new Error('ChildNodePart: startNode is not a comment node');
193
- }
194
-
195
- super();
196
-
197
- let serverSideRendered = false;
198
- if (startNode) {
199
- startNode.__part = this; // add Part to comment node for debugging in the browser
200
-
201
- const placeholder = startNode.data;
202
- const childNodes = [startNode];
203
- let childNode = startNode.nextSibling;
204
- while (childNode && childNode.data !== `/${placeholder}`) {
205
- childNodes.push(childNode);
206
- childNode = childNode.nextSibling;
207
- }
208
-
209
- const endNode = childNode;
210
- childNodes.push(endNode);
211
-
212
- // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
213
- if (childNodes.length > 2) {
214
- serverSideRendered = true;
215
- }
216
-
217
- this.childNodes = childNodes;
218
- this.startNode = startNode;
219
- this.endNode = endNode;
220
- }
221
-
222
- // value can become array | TemplatePart | any
223
- const initialValue = this.parseValue(value);
224
-
225
- if (this.endNode) {
226
- this.processor = processNodePart(this.endNode, serverSideRendered ? initialValue : undefined);
227
- }
228
-
229
- if (!serverSideRendered) {
230
- if (value instanceof TemplateResult || Array.isArray(value)) {
231
- this.updateParts(value);
232
- }
233
-
234
- // We need a childNodes list that is NOT live so that we don't lose elements when they get removed from the dom and we can (re)add them back in later.
235
- this.childNodes = [...this.childNodes];
236
- this.processor?.(initialValue);
237
- }
238
- }
239
-
240
- /**
241
- * @param {any} value
242
- * @return {*}
243
- */
244
- update(value) {
245
- const parsedValue = this.parseValue(value);
246
-
247
- if (value instanceof TemplateResult || Array.isArray(value)) {
248
- this.updateParts(value);
249
- }
250
-
251
- this.processor?.(parsedValue);
252
- }
253
-
254
- /**
255
- * @param {TemplateResult | any[]} value
256
- */
257
- updateParts(value) {
258
- if (value instanceof TemplateResult) {
259
- this.templatePart.update(value);
260
- }
261
- if (Array.isArray(value)) {
262
- for (let index = 0; index < value.length; index++) {
263
- this.parts[index]?.update(value[index]);
264
- }
265
- }
266
- }
267
-
268
- /**
269
- * @param {TemplateResult | any[] | any} value
270
- * @return {TemplatePart | any[] | any}
271
- */
272
- parseValue(value) {
273
- if (value instanceof TemplateResult) {
274
- if (!this.templatePart) {
275
- const templatePartCommentNodes = this.childNodes?.filter(
276
- (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
277
- );
278
- const startNode = templatePartCommentNodes[0];
279
- this.templatePart = new TemplatePart(startNode, value);
280
- }
281
- return this.templatePart;
282
- }
283
- if (Array.isArray(value)) {
284
- return this.parseArray(value);
285
- }
286
- return value;
287
- }
288
-
289
- /**
290
- * Nested TemplateResults values need to be unrolled in order for update functions to be able to process them
291
- * @param {any[]} values
292
- * @return {any[]}
293
- */
294
- parseArray(values) {
295
- const parsedValues = [];
296
- for (let index = 0; index < values.length; index++) {
297
- let value = values[index];
298
-
299
- if (value instanceof TemplateResult) {
300
- let templatePart = this.parts[index];
301
- if (!templatePart) {
302
- const templatePartCommentNodes = this.childNodes?.filter(
303
- (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
304
- );
305
- const startNode = templatePartCommentNodes[index];
306
- templatePart = new TemplatePart(startNode, value);
307
- this.parts[index] = templatePart;
308
- }
309
- parsedValues[index] = templatePart;
310
- } else if (Array.isArray(value)) {
311
- let childNodePart = this.parts[index];
312
- if (!childNodePart) {
313
- // TODO: this seems not correct :(
314
- const templatePartCommentNodes = this.childNodes?.filter(
315
- (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
316
- );
317
- const startNode = templatePartCommentNodes[index];
318
- childNodePart = new ChildNodePart(startNode, value);
319
- this.parts[index] = childNodePart;
320
- }
321
- parsedValues[index] = childNodePart;
322
- } else {
323
- parsedValues[index] = typeof value === 'function' ? value() : value;
324
- }
325
- }
326
-
327
- return parsedValues;
328
- }
168
+ /** @type {Node | undefined} */
169
+ startNode = undefined;
170
+
171
+ /** @type {Node | undefined} */
172
+ endNode = undefined;
173
+
174
+ // TODO: this is only for array values
175
+ /** @type {Part[]} */
176
+ parts = [];
177
+
178
+ // TODO: this is only for TemplatePart values
179
+ templatePart = undefined;
180
+
181
+ // TODO: maybe we can store a generic vale|parsedValue that can be either parts[] | templatePart | any
182
+
183
+ /** @type {Node[]} */
184
+ childNodes = [];
185
+
186
+ /**
187
+ * @param {Node} startNode - the start comment marker node
188
+ * @param {any | any[]} value
189
+ */
190
+ constructor(startNode, value) {
191
+ if (startNode && startNode?.nodeType !== COMMENT_NODE) {
192
+ throw new Error('ChildNodePart: startNode is not a comment node');
193
+ }
194
+
195
+ super();
196
+
197
+ let serverSideRendered = false;
198
+ if (startNode) {
199
+ startNode.__part = this; // add Part to comment node for debugging in the browser
200
+
201
+ const placeholder = startNode.data;
202
+ const childNodes = [startNode];
203
+ let childNode = startNode.nextSibling;
204
+ while (childNode && childNode.data !== `/${placeholder}`) {
205
+ childNodes.push(childNode);
206
+ childNode = childNode.nextSibling;
207
+ }
208
+
209
+ const endNode = childNode;
210
+ childNodes.push(endNode);
211
+
212
+ // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
213
+ if (childNodes.length > 2) {
214
+ serverSideRendered = true;
215
+ }
216
+
217
+ this.childNodes = childNodes;
218
+ this.startNode = startNode;
219
+ this.endNode = endNode;
220
+ }
221
+
222
+ // value can become array | TemplatePart | any
223
+ const initialValue = this.parseValue(value);
224
+
225
+ if (this.endNode) {
226
+ this.processor = processNodePart(this.endNode, serverSideRendered ? initialValue : undefined);
227
+ }
228
+
229
+ if (!serverSideRendered) {
230
+ if (value instanceof TemplateResult || Array.isArray(value)) {
231
+ this.updateParts(value);
232
+ }
233
+
234
+ // We need a childNodes list that is NOT live so that we don't lose elements when they get removed from the dom and we can (re)add them back in later.
235
+ this.childNodes = [...this.childNodes];
236
+ this.processor?.(initialValue);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * @param {any} value
242
+ * @return {*}
243
+ */
244
+ update(value) {
245
+ const parsedValue = this.parseValue(value);
246
+
247
+ if (value instanceof TemplateResult || Array.isArray(value)) {
248
+ this.updateParts(value);
249
+ }
250
+
251
+ this.processor?.(parsedValue);
252
+ }
253
+
254
+ /**
255
+ * @param {TemplateResult | any[]} value
256
+ */
257
+ updateParts(value) {
258
+ if (value instanceof TemplateResult) {
259
+ this.templatePart.update(value);
260
+ }
261
+ if (Array.isArray(value)) {
262
+ for (let index = 0; index < value.length; index++) {
263
+ this.parts[index]?.update(value[index]);
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * @param {TemplateResult | any[] | any} value
270
+ * @return {TemplatePart | any[] | any}
271
+ */
272
+ parseValue(value) {
273
+ if (value instanceof TemplateResult) {
274
+ if (!this.templatePart) {
275
+ const templatePartCommentNodes = this.childNodes?.filter(
276
+ (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
277
+ );
278
+ const startNode = templatePartCommentNodes[0];
279
+ this.templatePart = new TemplatePart(startNode, value);
280
+ }
281
+ return this.templatePart;
282
+ }
283
+ if (Array.isArray(value)) {
284
+ return this.parseArray(value);
285
+ }
286
+ return value;
287
+ }
288
+
289
+ /**
290
+ * Nested TemplateResults values need to be unrolled in order for update functions to be able to process them
291
+ * @param {any[]} values
292
+ * @return {any[]}
293
+ */
294
+ parseArray(values) {
295
+ const parsedValues = [];
296
+ for (let index = 0; index < values.length; index++) {
297
+ let value = values[index];
298
+
299
+ if (value instanceof TemplateResult) {
300
+ let templatePart = this.parts[index];
301
+ if (!templatePart) {
302
+ const templatePartCommentNodes = this.childNodes?.filter(
303
+ (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
304
+ );
305
+ const startNode = templatePartCommentNodes[index];
306
+ templatePart = new TemplatePart(startNode, value);
307
+ this.parts[index] = templatePart;
308
+ }
309
+ parsedValues[index] = templatePart;
310
+ } else if (Array.isArray(value)) {
311
+ let childNodePart = this.parts[index];
312
+ if (!childNodePart) {
313
+ // TODO: this seems not correct :( (maybe Template vs DOM Parts !?)
314
+ const templatePartCommentNodes = this.childNodes?.filter(
315
+ (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
316
+ );
317
+ const startNode = templatePartCommentNodes[index];
318
+ childNodePart = new ChildNodePart(startNode, value);
319
+ this.parts[index] = childNodePart;
320
+ }
321
+ parsedValues[index] = childNodePart;
322
+ } else {
323
+ parsedValues[index] = typeof value === 'function' ? value() : value;
324
+ }
325
+ }
326
+
327
+ return parsedValues;
328
+ }
329
329
  }