@webtides/element-js 0.7.4 → 1.0.0-alpha.10

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 (51) hide show
  1. package/README.md +2 -2
  2. package/docs/README.md +174 -0
  3. package/docs/concepts/classes.md +167 -0
  4. package/docs/concepts/hierarchy.md +61 -0
  5. package/docs/concepts/lifecycle.md +45 -0
  6. package/docs/concepts/reactivity.md +14 -0
  7. package/docs/features/computed-properties.md +28 -0
  8. package/docs/features/dependency-injection-context-protocol.md +132 -0
  9. package/docs/features/events.md +155 -0
  10. package/docs/features/global-styles.md +131 -0
  11. package/docs/features/methods.md +34 -0
  12. package/docs/features/properties.md +56 -0
  13. package/docs/features/refs.md +47 -0
  14. package/docs/features/renderless.md +70 -0
  15. package/docs/features/store.md +143 -0
  16. package/docs/features/styles.md +169 -0
  17. package/docs/features/templates.md +308 -0
  18. package/docs/features/watch.md +54 -0
  19. package/docs/getting-started/upgrade-to-v1.md +243 -0
  20. package/docs/guide/guide.md +112 -0
  21. package/index.js +9 -2
  22. package/package.json +19 -27
  23. package/src/BaseElement.js +176 -86
  24. package/src/StyledElement.js +66 -55
  25. package/src/TemplateElement.js +34 -8
  26. package/src/dom-parts/AttributePart.js +167 -0
  27. package/src/dom-parts/ChildNodePart.js +329 -0
  28. package/src/dom-parts/NodePart.js +38 -0
  29. package/src/dom-parts/Part.js +12 -0
  30. package/src/dom-parts/TemplatePart.js +134 -0
  31. package/src/dom-parts/TemplateResult.js +342 -0
  32. package/src/dom-parts/directives.js +111 -0
  33. package/src/dom-parts/html.js +12 -0
  34. package/src/dom-parts/render.js +10 -0
  35. package/src/util/AttributeParser.js +62 -29
  36. package/src/util/DOMHelper.js +253 -5
  37. package/src/util/Directive.js +42 -0
  38. package/src/util/GlobalStylesStore.js +66 -0
  39. package/src/util/SerializeStateHelper.js +103 -0
  40. package/src/util/Store.js +92 -9
  41. package/src/util/defineElement.js +2 -2
  42. package/src/util/toString.js +2 -2
  43. package/CHANGELOG.md +0 -154
  44. package/dist/index.js +0 -1673
  45. package/dist/index.js.map +0 -7
  46. package/src/renderer/vanilla/TemplateElement.js +0 -50
  47. package/src/renderer/vanilla/index.js +0 -18
  48. package/src/renderer/vanilla/util/directives.js +0 -78
  49. package/src/renderer/vanilla/util/html.js +0 -65
  50. package/src/renderer/vanilla/util/render.js +0 -149
  51. package/src/util/i18n.js +0 -20
@@ -0,0 +1,167 @@
1
+ import { Part } from './Part.js';
2
+
3
+ /**
4
+ * @param {Element} node
5
+ * @param {String} name
6
+ * @param {Boolean} oldValue
7
+ * @return {(function(*): void)|*}
8
+ */
9
+ const processBooleanAttribute = (node, name, oldValue) => {
10
+ // TODO: It would be better if the ?boolean= attribute would not be there in the first place...
11
+ node.removeAttribute(`?${name}`);
12
+ return (newValue) => {
13
+ const value = !!newValue?.valueOf() && newValue !== 'false';
14
+ if (oldValue !== value) {
15
+ node.toggleAttribute(name, (oldValue = !!value));
16
+ }
17
+ };
18
+ };
19
+
20
+ /**
21
+ * @param {Element} node
22
+ * @param {String} name
23
+ * @return {(function(*): void)|*}
24
+ */
25
+ const processPropertyAttribute = (node, name) => {
26
+ // TODO: It would be better if the .property= attribute would not be there in the first place...
27
+ node.removeAttribute(`.${name}`);
28
+ return (value) => {
29
+ node[name] = value;
30
+ };
31
+ };
32
+
33
+ /**
34
+ * @param {Element} node
35
+ * @param {String} name
36
+ * @return {(function(*): void)|*}
37
+ */
38
+ const processEventAttribute = (node, name) => {
39
+ // TODO: It would be better if the event attribute would not be there in the first place...
40
+ node.removeAttribute(name);
41
+
42
+ let oldValue = undefined;
43
+ let type = name.startsWith('@') ? name.slice(1) : name.toLowerCase().slice(2);
44
+
45
+ return (newValue) => {
46
+ if (oldValue !== newValue) {
47
+ if (oldValue) node.removeEventListener(type, oldValue);
48
+ if ((oldValue = newValue)) node.addEventListener(type, oldValue);
49
+ }
50
+ };
51
+ };
52
+
53
+ /**
54
+ * @param {Element} node
55
+ * @param {String} name
56
+ * @return {(function(*): void)|*}
57
+ */
58
+ const processAttribute = (node, name) => {
59
+ let oldValue,
60
+ orphan = true;
61
+ const attributeNode = globalThis.document?.createAttributeNS(null, name);
62
+ return (newValue) => {
63
+ const value = newValue?.valueOf();
64
+ if (oldValue !== value) {
65
+ if ((oldValue = value) == null) {
66
+ if (!orphan) {
67
+ node.removeAttributeNode(attributeNode);
68
+ orphan = true;
69
+ }
70
+ } else {
71
+ attributeNode.value = value;
72
+ if (orphan) {
73
+ node.setAttributeNodeNS(attributeNode);
74
+ orphan = false;
75
+ }
76
+ }
77
+ }
78
+ };
79
+ };
80
+
81
+ /** @type {Map<Element, AttributePart>} */
82
+ const attributePartsCache = new WeakMap();
83
+
84
+ /**
85
+ * @param {Element} node
86
+ * @param {String} name
87
+ * @return {(function(*): void)|*}
88
+ */
89
+ export const processAttributePart = (node, name) => {
90
+ // boolean attribute: ?boolean=${...}
91
+ if (name.startsWith('?')) {
92
+ return processBooleanAttribute(node, name.slice(1), false);
93
+ }
94
+
95
+ // property attribute: .property=${...}
96
+ if (name.startsWith('.')) {
97
+ return processPropertyAttribute(node, name.slice(1));
98
+ }
99
+
100
+ // event attribute: @event=${...} || "old school" event attribute: onevent=${...}
101
+ if (name.startsWith('@') || name.startsWith('on')) {
102
+ return processEventAttribute(node, name);
103
+ }
104
+
105
+ // normal "string" attribute: attribute=${...}
106
+ return processAttribute(node, name);
107
+ };
108
+
109
+ /**
110
+ * For updating a single attribute
111
+ */
112
+ export class AttributePart extends Part {
113
+ name = undefined;
114
+ interpolations = 1;
115
+ values = [];
116
+ currentValueIndex = 0;
117
+ initialValue = undefined;
118
+
119
+ /**
120
+ * @param {Comment} node
121
+ * @param {String} name
122
+ * @param {String} initialValue
123
+ */
124
+ constructor(node, name, initialValue) {
125
+ // If we have multiple attribute parts with the same name, it means we have multiple
126
+ // interpolations inside that attribute. Instead of creating a new part, we will return the same
127
+ // as before and let it defer the update until the last interpolation gets updated
128
+ const attributePart = attributePartsCache.get(node.nextElementSibling);
129
+ if (attributePart && attributePart.name === name) {
130
+ attributePart.interpolations++;
131
+ node.__part = attributePart; // add Part to comment node for debugging in the browser
132
+ return attributePart;
133
+ }
134
+
135
+ super();
136
+ this.name = name;
137
+ this.initialValue = initialValue;
138
+ this.processor = processAttributePart(node.nextElementSibling, this.name);
139
+ node.__part = this; // add Part to comment node for debugging in the browser
140
+ attributePartsCache.set(node.nextElementSibling, this);
141
+ }
142
+
143
+ /**
144
+ * @param {string | number | bigint | boolean | undefined | symbol | null} value
145
+ */
146
+ update(value) {
147
+ // If we only have one sole interpolation, we can just apply the update
148
+ if (this.initialValue === '\x03') {
149
+ this.processor(value);
150
+ return;
151
+ }
152
+
153
+ // Instead of applying the update immediately, we check if the part has multiple interpolations
154
+ // and store the values for each interpolation in a list
155
+ this.values[this.currentValueIndex++] = value;
156
+
157
+ // Only the last call to update (for the last interpolation) will actually trigger the update
158
+ // on the DOM processor. Here we can reset everything before the next round of updates
159
+ if (this.interpolations === this.currentValueIndex) {
160
+ this.currentValueIndex = 0;
161
+ let replaceIndex = 0;
162
+ // Note: this will coarse the values into strings, but it's probably ok since there can only be multiple values in string attributes?!
163
+ const parsedValue = this.initialValue.replace(/\x03/g, () => this.values[replaceIndex++]);
164
+ this.processor(parsedValue);
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,329 @@
1
+ import { COMMENT_NODE, ELEMENT_NODE } from '../util/DOMHelper.js';
2
+ import { Part } from './Part.js';
3
+ import { TemplateResult } from './TemplateResult.js';
4
+ import { TemplatePart } from './TemplatePart.js';
5
+
6
+ /**
7
+ * @param {Node | TemplatePart | any[]} node
8
+ * @return {ChildNode}
9
+ */
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;
26
+ };
27
+
28
+ /**
29
+ * @param {Element} commentNode
30
+ */
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
+ }
41
+ };
42
+
43
+ /**
44
+ * @param {Node} parentNode
45
+ * @param {Node[]} domChildNodes
46
+ * @param {Node[]} templateChildNodes
47
+ * @param {Node} anchorNode
48
+ * @return {Node[]}
49
+ */
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;
86
+ };
87
+
88
+ /**
89
+ * @param {Node} comment
90
+ * @param {TemplatePart | any[] | any} initialValue
91
+ * @return {(function(*): void)|*}
92
+ */
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;
162
+ };
163
+
164
+ /**
165
+ * For updating a text node, a sequence of child nodes or an array of `any` values
166
+ */
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
+ }
329
+ }
@@ -0,0 +1,38 @@
1
+ import { Part } from './Part.js';
2
+ import { isObjectLike } from '../util/AttributeParser.js';
3
+
4
+ /**
5
+ * For updating a directive on a node
6
+ */
7
+ export class NodePart extends Part {
8
+ /** @type {Node} */
9
+ node = undefined;
10
+
11
+ /** @type {Directive} */
12
+ directive = undefined;
13
+
14
+ /**
15
+ * @param {Node} node
16
+ * @param {() => {}} initialValue
17
+ */
18
+ constructor(node, initialValue) {
19
+ if (!isObjectLike(initialValue) && !initialValue.directiveClass) {
20
+ throw new Error(
21
+ 'NodePart: value is not a wrapped directive function. You must wrap you custom directive class with the defineDirective function.',
22
+ );
23
+ }
24
+ super();
25
+ node.__part = this; // add Part to comment node for debugging in the browser
26
+ this.node = node.nextElementSibling;
27
+ const { directiveClass, values } = initialValue;
28
+ this.directive = new directiveClass(node.nextElementSibling);
29
+ }
30
+
31
+ /**
32
+ * @param {() => {}} value
33
+ */
34
+ update(value) {
35
+ const { directiveClass, values } = value;
36
+ this.directive.update(...values);
37
+ }
38
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @abstract
3
+ */
4
+ export class Part {
5
+ processor = undefined;
6
+
7
+ /**
8
+ * @abstract
9
+ * @param {TemplateResult | any[] | any} value
10
+ */
11
+ update(value) {}
12
+ }
@@ -0,0 +1,134 @@
1
+ import { COMMENT_NODE, convertStringToTemplate } from '../util/DOMHelper.js';
2
+ import { Part } from './Part.js';
3
+ import { TemplateResult } from './TemplateResult.js';
4
+ import { AttributePart } from './AttributePart.js';
5
+ import { ChildNodePart } from './ChildNodePart.js';
6
+ import { NodePart } from './NodePart.js';
7
+
8
+ /** @type {Map<TemplateStringsArray, Part[]>} */
9
+ const partsCache = new WeakMap();
10
+
11
+ /** @type {Map<TemplateStringsArray, DocumentFragment>} */
12
+ const fragmentsCache = new WeakMap();
13
+
14
+ export class TemplatePart extends Part {
15
+ /** @type {Part[]} */
16
+ parts = [];
17
+
18
+ /** @type {TemplateStringsArray} */
19
+ strings = undefined;
20
+
21
+ /** @type {Node[]} */
22
+ childNodes = [];
23
+
24
+ /**
25
+ * @param {Node} startNode - the start comment marker node
26
+ * @param {TemplateResult} value
27
+ */
28
+ constructor(startNode, value) {
29
+ if (startNode && startNode?.nodeType !== COMMENT_NODE) {
30
+ throw new Error('TemplatePart: startNode is not a comment node');
31
+ }
32
+
33
+ super();
34
+
35
+ let serverSideRendered = false;
36
+ if (startNode) {
37
+ startNode.__part = this; // add Part to comment node for debugging in the browser
38
+
39
+ const placeholder = startNode.data;
40
+ const childNodes = [startNode];
41
+ let childNode = startNode.nextSibling;
42
+ while (childNode && childNode.data !== `/${placeholder}`) {
43
+ childNodes.push(childNode);
44
+ childNode = childNode.nextSibling;
45
+ }
46
+
47
+ const endNode = childNode;
48
+ childNodes.push(endNode);
49
+
50
+ // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
51
+ if (childNodes.length > 2) {
52
+ serverSideRendered = true;
53
+ this.childNodes = childNodes;
54
+ }
55
+ }
56
+
57
+ this.parseValue(value);
58
+
59
+ if (!serverSideRendered) {
60
+ this.updateParts(value.values);
61
+ // We need a childNodes list that is NOT live so that we don't loose elements when they get removed from the dom and we can (re)add them back in later.
62
+ this.childNodes = [...this.childNodes];
63
+ }
64
+ }
65
+
66
+ /**
67
+ * @param {TemplateResult} value
68
+ */
69
+ update(value) {
70
+ this.parseValue(value);
71
+ this.updateParts(value.values);
72
+ }
73
+
74
+ /**
75
+ * @param {any[]} values
76
+ */
77
+ updateParts(values) {
78
+ for (let index = 0; index < values.length; index++) {
79
+ this.parts[index]?.update(values[index]);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @param {TemplateResult} templateResult
85
+ */
86
+ parseValue(templateResult) {
87
+ if (this.strings !== templateResult.strings) {
88
+ if (this.childNodes.length === 0) {
89
+ let fragment = fragmentsCache.get(templateResult.strings);
90
+ if (!fragment) {
91
+ fragment = this.parseFragment(templateResult);
92
+ fragmentsCache.set(templateResult.strings, fragment);
93
+ }
94
+ const importedFragment = globalThis.document?.importNode(fragment, true);
95
+ this.childNodes = importedFragment.childNodes;
96
+ }
97
+
98
+ let parts = partsCache.get(templateResult.strings);
99
+ if (!parts) {
100
+ parts = templateResult.parseParts(this.childNodes);
101
+ partsCache.set(templateResult.strings, parts);
102
+ }
103
+
104
+ this.parts = parts
105
+ .map((part) => {
106
+ // We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
107
+ part.node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
108
+ return part;
109
+ })
110
+ .map((part, index) => {
111
+ if (part.type === 'node') {
112
+ return new ChildNodePart(part.node, templateResult.values[index]);
113
+ }
114
+ if (part.type === 'attribute') {
115
+ return new AttributePart(part.node, part.name, part.initialValue);
116
+ }
117
+ if (part.type === 'directive') {
118
+ return new NodePart(part.node, templateResult.values[index]);
119
+ }
120
+ throw `cannot map part: ${part}`;
121
+ });
122
+ this.strings = templateResult.strings;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @param {TemplateResult} templateResult
128
+ * @return {DocumentFragment}
129
+ */
130
+ parseFragment(templateResult) {
131
+ const templateString = templateResult.templateString;
132
+ return convertStringToTemplate(templateString);
133
+ }
134
+ }