@webtides/element-js 1.1.7 → 1.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "module": "index.js",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -2,211 +2,204 @@ import { COMMENT_NODE, ELEMENT_NODE } from '../util/DOMHelper.js';
2
2
  import { Part } from './Part.js';
3
3
  import { TemplateResult } from './TemplateResult.js';
4
4
  import { TemplatePart } from './TemplatePart.js';
5
+ import { PartMarkers } from './PartMarkers.js';
5
6
 
6
7
  /**
7
- * @param {Element} commentNode
8
- * @param {Element[]} nodes
8
+ * @param {Comment} commentNode
9
+ * @param {Node[]} nodes
9
10
  */
10
- const insertNodesBetweenComments = (commentNode, nodes) => {
11
+ export const insertNodesBetweenComments = (commentNode, nodes) => {
11
12
  for (const node of nodes) {
12
13
  commentNode.parentNode.insertBefore(node, commentNode);
13
14
  }
14
15
  };
15
16
 
16
17
  /**
17
- * @param {Element} commentNode
18
+ * @param {Comment} commentNode
19
+ * @param {Node[]} nodes
18
20
  */
19
- const removeNodesBetweenComments = (commentNode) => {
20
- while (commentNode.previousSibling) {
21
- if (
22
- commentNode.previousSibling.nodeType === COMMENT_NODE &&
23
- commentNode.previousSibling.data === commentNode.data.replace('/', '')
24
- ) {
25
- break;
26
- }
27
- commentNode.parentNode.removeChild(commentNode.previousSibling);
21
+ export const replaceNodesBetweenComments = (commentNode, nodes) => {
22
+ removeNodesBetweenComments(commentNode);
23
+ for (const node of nodes) {
24
+ commentNode.parentNode.insertBefore(node, commentNode);
25
+ }
26
+ };
27
+
28
+ /**
29
+ * @param {Comment} commentNode
30
+ */
31
+ export const removeNodesBetweenComments = (commentNode) => {
32
+ const nodes = getNodesBetweenComments(commentNode);
33
+ for (const node of nodes) {
34
+ commentNode.parentNode.removeChild(node);
28
35
  }
29
36
  };
30
37
 
31
38
  /**
32
- * @param {Node} parentNode
33
- * @param {Node[]} domChildNodes
34
- * @param {Node[]} templateChildNodes
35
- * @param {Node} anchorNode
39
+ * @param {Comment} commentNode
40
+ * @param {boolean} includeComments
36
41
  * @return {Node[]}
37
42
  */
38
- const diffChildNodes = function (parentNode, domChildNodes, templateChildNodes, anchorNode) {
39
- // TODO: when diffing arrays there is sometimes two lists of TemplateParts instead of lists of childNodes
40
- const offset = domChildNodes.includes(anchorNode) ? 1 : 0;
41
-
42
- // release first level references to prevent cache mutation
43
- const clonedDomChildNodes = [...domChildNodes];
44
- const clonedTemplateChildNodes = [...templateChildNodes];
45
-
46
- // We always want to have the two arrays (domChildNodes and cloneTemplateChildNodes) to have the same length.
47
- // We also don't ever want to replace/diff the last node of the domChildNodes as it will always be the
48
- // closing comment marker (/template-part) aka the anchorNode. So we push it to the end by inserting undefined items.
49
- if (clonedTemplateChildNodes.length > clonedDomChildNodes.length) {
50
- const nodesToFill = clonedTemplateChildNodes.length - clonedDomChildNodes.length;
51
- for (let i = 0; i < nodesToFill; i++) {
52
- clonedDomChildNodes.splice(clonedDomChildNodes.length - offset, 0, undefined);
43
+ export const getNodesBetweenComments = (commentNode, includeComments = false) => {
44
+ const nodes = [];
45
+ let currentNode = includeComments ? commentNode : commentNode.previousSibling;
46
+ let nestingLevel = 0;
47
+
48
+ while (currentNode) {
49
+ if (includeComments) nodes.unshift(currentNode);
50
+
51
+ if (currentNode.nodeType === COMMENT_NODE && currentNode.data === commentNode.data) {
52
+ nestingLevel++;
53
53
  }
54
- }
55
54
 
56
- if (clonedDomChildNodes.length > clonedTemplateChildNodes.length) {
57
- const nodesToFill = clonedDomChildNodes.length - clonedTemplateChildNodes.length;
58
- for (let i = 0; i < nodesToFill; i++) {
59
- clonedTemplateChildNodes.splice(clonedTemplateChildNodes.length - offset, 0, undefined);
55
+ if (currentNode.nodeType === COMMENT_NODE && currentNode.data === commentNode.data.replace('/', '')) {
56
+ if (nestingLevel === 0) {
57
+ break;
58
+ }
59
+ nestingLevel--;
60
60
  }
61
+
62
+ if (!includeComments) nodes.unshift(currentNode);
63
+ currentNode = currentNode.previousSibling;
61
64
  }
65
+ return nodes;
66
+ };
62
67
 
63
- const length = clonedDomChildNodes.length;
68
+ /**
69
+ * @param {Node[]} newChildNodes
70
+ * @param {Comment} anchorNode
71
+ */
72
+ const diffChildNodes = function (newChildNodes, anchorNode) {
73
+ const parentNode = anchorNode.parentNode;
74
+ const oldChildNodes = getNodesBetweenComments(anchorNode);
75
+
76
+ const length = Math.max(oldChildNodes.length, newChildNodes.length);
64
77
  // Diff each node in the child node lists
65
78
  for (let index = 0; index < length; index++) {
66
- let domChildNode = clonedDomChildNodes[index];
67
- let templateChildNode = clonedTemplateChildNodes[index];
79
+ let oldChildNode = oldChildNodes[index];
80
+ let newChildNode = newChildNodes[index];
68
81
 
69
- // If the DOM node doesn't exist, append/copy the template node
70
- if (domChildNode === undefined) {
71
- if (typeof templateChildNode === 'object' && 'ELEMENT_NODE' in templateChildNode) {
72
- parentNode.insertBefore(templateChildNode, anchorNode);
73
- } else {
74
- // TODO: this might not be performant?! Can we maybe handle this in processDomNode?!
75
- // TODO: I think that we need to make ChildNodeParts from all primitive values as well
76
- parentNode.insertBefore(document.createTextNode(templateChildNode), anchorNode);
77
- }
82
+ if (newChildNode && !newChildNode.nodeType) {
83
+ throw new Error('ChildNodePart: newChildNode is not a node');
84
+ }
85
+
86
+ // If the DOM node doesn't exist, append/copy the new node
87
+ if (oldChildNode === undefined) {
88
+ parentNode.insertBefore(newChildNode, anchorNode);
78
89
  continue;
79
90
  }
80
91
 
81
- if (templateChildNode === undefined) {
82
- // If the template node doesn't exist, remove the node in the DOM
83
- if ('ELEMENT_NODE' in domChildNode) {
84
- // remove dom node
85
- parentNode.removeChild(domChildNode);
86
- } else if (Array.isArray(domChildNode) || domChildNode instanceof TemplatePart) {
87
- // remove childNodes
88
- const childNodes = Array.isArray(domChildNode) ? domChildNode : domChildNode.childNodes;
89
- for (const childNode of childNodes) {
90
- childNode.remove();
91
- }
92
- } else {
93
- console.log('This should not happen... what do we do about this node?!', { domChildNode });
94
- }
92
+ // If the new node doesn't exist, remove the node in the DOM
93
+ if (newChildNode === undefined) {
94
+ parentNode.removeChild(oldChildNode);
95
95
  continue;
96
96
  }
97
97
 
98
- // If DOM node is equal to the template node, don't do anything in the DOM
99
- if (
100
- domChildNode === templateChildNode ||
101
- (domChildNode.nodeType && templateChildNode.nodeType && domChildNode.isEqualNode(templateChildNode))
102
- ) {
103
- // but we need to assign the current DOM node into the templateChildNodes
104
- // as they will be returned here at the end and become the domNodes for the next diffing.
105
- const childIndex = templateChildNodes.indexOf(templateChildNode);
106
- templateChildNodes[childIndex] = domChildNode;
98
+ // If DOM node is equal to the new node, don't do anything in the DOM
99
+ if (oldChildNode === newChildNode || oldChildNode.isEqualNode(newChildNode)) {
107
100
  continue;
108
101
  }
109
102
 
110
103
  // Everything else is somehow different and can be replaced
111
- if (parentNode.contains(domChildNode)) {
112
- parentNode.replaceChild(templateChildNode, domChildNode);
113
- } else {
114
- console.warn('This should not happen', {
115
- parentNode,
116
- 'parentNode.contains(domChildNode)': parentNode.contains(domChildNode),
117
- templateChildNode,
118
- domChildNode,
119
- });
120
- }
104
+ parentNode.replaceChild(newChildNode, oldChildNode);
121
105
  }
122
- return templateChildNodes;
106
+ };
107
+
108
+ function getType(value) {
109
+ if (value instanceof TemplatePart) return 'templatePart';
110
+ if (value === null || value === undefined) return 'empty';
111
+ if (Array.isArray(value)) return value.length === 0 ? 'empty' : 'array';
112
+ if (value?.nodeType) return 'node';
113
+ return 'text';
114
+ }
115
+
116
+ // TODO: handle funcitons?! Should be handled in parseArray!!!
117
+ const unwrapArray = (nodes) => {
118
+ // we have to unwrap any complex objects inside the array so that we can diff arrays of childNodes
119
+ return nodes.flatMap((value) => {
120
+ if (value instanceof TemplatePart) {
121
+ return value.childNodes;
122
+ }
123
+ if (Array.isArray(value)) {
124
+ return unwrapArray(value);
125
+ }
126
+ if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
127
+ return document.createTextNode(value);
128
+ }
129
+ return value;
130
+ });
123
131
  };
124
132
 
125
133
  /**
126
- * @param {Node} comment
134
+ * @param {Comment} comment
127
135
  * @param {TemplatePart | any[] | any} initialValue
136
+ * @param {boolean} serverSideRendered
128
137
  * @return {(function(*): void)|*}
129
138
  */
130
- export const processNodePart = (comment, initialValue) => {
131
- let nodes = initialValue instanceof TemplatePart ? initialValue.childNodes : [];
132
- let oldValue = initialValue instanceof TemplatePart ? initialValue.strings : initialValue;
139
+ export const processNodePart = (comment, initialValue, serverSideRendered = false) => {
140
+ let oldValueType = 'empty';
141
+ let oldValue = undefined;
133
142
  // 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.
134
- let cachedTextNode = undefined;
143
+ // TODO: when server side rendered, we need to get the text node from the DOM no?!
144
+ let cachedTextNode = globalThis.document?.createTextNode('');
145
+
146
+ if (serverSideRendered) {
147
+ oldValueType = getType(initialValue);
148
+ oldValue = initialValue instanceof TemplatePart ? initialValue.strings : initialValue;
149
+ }
135
150
 
136
151
  const processNodeValue = (newValue) => {
137
- switch (typeof newValue) {
138
- // primitives are handled as text content
139
- case 'string':
140
- case 'number':
141
- case 'boolean':
142
- if (oldValue !== newValue) {
143
- oldValue = newValue;
144
- if (!cachedTextNode) cachedTextNode = globalThis.document?.createTextNode('');
145
- cachedTextNode.data = newValue;
146
-
147
- if (comment.previousSibling?.data === comment.data.replace('/', '')) {
148
- // the part is empty - we haven't rendered it yet
149
- comment.parentNode.insertBefore(cachedTextNode, comment);
150
- } else {
151
- // the part was already rendered (either server side or on the client)
152
- comment.previousSibling.data = newValue;
153
- }
154
-
155
- nodes = [cachedTextNode];
156
- }
157
- break;
158
- // null (= typeof "object") and undefined are used to clean up previous content
159
- case 'object':
160
- case 'undefined':
161
- if (newValue === null || newValue === undefined) {
162
- if (oldValue !== newValue) {
163
- oldValue = newValue;
164
- removeNodesBetweenComments(comment);
165
- nodes = [];
166
- }
167
- break;
168
- }
169
- if (Array.isArray(newValue)) {
170
- if (newValue.length === 0) {
171
- removeNodesBetweenComments(comment);
172
- nodes = [];
173
- } else {
174
- // TODO: this will always diff the arrays although they might not have changed...
175
- const unwrapArray = (nodes) => {
176
- // we have to unwrap any complex objects inside the array so that we can diff arrays of childNodes
177
- return nodes.flatMap((value) => {
178
- if (value instanceof TemplatePart) {
179
- return value.childNodes;
180
- }
181
- if (Array.isArray(value)) {
182
- return unwrapArray(value);
183
- }
184
- return value;
185
- });
186
- };
187
- nodes = diffChildNodes(comment.parentNode, nodes, unwrapArray(newValue), comment);
188
- }
189
- oldValue = newValue;
190
- break;
191
- }
152
+ let newValueType = getType(newValue);
192
153
 
193
- if (newValue instanceof TemplatePart && oldValue !== newValue.strings) {
194
- // if the new value is a node or a fragment, and it's different from the live node, then it's diffed.
195
- // static strings have changed in tpl part
196
- oldValue = newValue.strings;
197
- nodes = diffChildNodes(comment.parentNode, nodes, newValue.childNodes, comment);
198
- } else if (oldValue !== newValue && 'ELEMENT_NODE' in newValue) {
199
- // DOM Node changed, needs diffing
200
- oldValue = newValue;
201
- nodes = diffChildNodes(comment.parentNode, nodes, [newValue], comment);
202
- }
203
- break;
204
- case 'function':
205
- processNodeValue(newValue(comment));
206
- break;
154
+ if (oldValueType === newValueType && oldValue === newValue) return;
155
+
156
+ if (oldValueType !== newValueType) {
157
+ if (newValueType === 'empty') {
158
+ removeNodesBetweenComments(comment);
159
+ } else {
160
+ const getNodesForType = (value, type) => {
161
+ if (type === 'templatePart') return value.childNodes;
162
+ if (type === 'array') return unwrapArray(value);
163
+ if (type === 'node') return [value];
164
+
165
+ // Every else falls back to rendering a text node
166
+ cachedTextNode.data = value;
167
+ return [cachedTextNode];
168
+ };
169
+ replaceNodesBetweenComments(comment, getNodesForType(newValue, newValueType));
170
+ }
171
+
172
+ oldValueType = newValueType;
173
+ oldValue = newValue;
174
+ return;
175
+ }
176
+
177
+ if (newValueType === 'text') {
178
+ cachedTextNode.data = newValue;
179
+
180
+ oldValue = newValue;
181
+ return;
182
+ }
183
+
184
+ if (newValueType === 'array') {
185
+ diffChildNodes(unwrapArray(newValue), comment);
186
+
187
+ oldValue = newValue;
188
+ return;
189
+ }
190
+
191
+ if (newValueType === 'node') {
192
+ diffChildNodes([newValue], comment);
193
+
194
+ oldValue = newValue;
207
195
  }
208
196
  };
209
197
 
198
+ if (!serverSideRendered) {
199
+ // initial rendering
200
+ processNodeValue(initialValue);
201
+ }
202
+
210
203
  return processNodeValue;
211
204
  };
212
205
 
@@ -214,69 +207,35 @@ export const processNodePart = (comment, initialValue) => {
214
207
  * For updating a text node, a sequence of child nodes or an array of `any` values
215
208
  */
216
209
  export class ChildNodePart extends Part {
217
- // TODO: this is only for array values
218
- /** @type {Part[]} */
219
- parts = [];
220
-
221
- // TODO: this is only for TemplatePart values
222
- templatePart = undefined;
210
+ /** @type {PartMarkers|undefined} */
211
+ markers;
223
212
 
224
- // TODO: maybe we can store a generic vale|parsedValue that can be either parts[] | templatePart | any
213
+ /** @type {TemplatePart|undefined} */
214
+ templatePart;
225
215
 
226
- /** @type {Node[]} */
227
- childNodes = [];
216
+ /** @type {Part[]} */
217
+ arrayParts = [];
228
218
 
229
219
  /**
230
220
  * @param {Node} startNode - the start comment marker node
231
221
  * @param {any | any[]} value
232
222
  */
233
223
  constructor(startNode, value) {
234
- if (startNode && startNode?.nodeType !== COMMENT_NODE) {
235
- throw new Error('ChildNodePart: startNode is not a comment node');
224
+ if (!startNode || startNode?.nodeType !== COMMENT_NODE) {
225
+ throw new Error(
226
+ startNode ? 'ChildNodePart: startNode is undefined' : 'ChildNodePart: startNode is not a comment node',
227
+ );
236
228
  }
237
229
 
238
230
  super();
239
231
 
240
- let endNode = undefined;
241
- let serverSideRendered = false;
242
- if (startNode) {
243
- startNode.__part = this; // add Part to comment node for debugging in the browser
244
-
245
- const placeholder = startNode.data;
246
- const childNodes = [startNode];
247
- let childNode = startNode.nextSibling;
248
- while (childNode && childNode.data !== `/${placeholder}`) {
249
- childNodes.push(childNode);
250
- childNode = childNode.nextSibling;
251
- }
252
-
253
- endNode = childNode;
254
- childNodes.push(endNode);
255
-
256
- // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
257
- if (childNodes.length > 2) {
258
- serverSideRendered = true;
259
- }
260
-
261
- this.childNodes = childNodes;
262
- }
232
+ startNode.__part = this; // add Part to comment node for debugging in the browser
233
+ this.markers = PartMarkers.createFromStartNode(startNode);
263
234
 
264
235
  // value can become array | TemplatePart | any
265
236
  const initialValue = this.parseValue(value);
266
237
 
267
- if (endNode) {
268
- this.processor = processNodePart(endNode, serverSideRendered ? initialValue : undefined);
269
- }
270
-
271
- if (!serverSideRendered) {
272
- if (value instanceof TemplateResult || Array.isArray(value)) {
273
- this.updateParts(value);
274
- }
275
-
276
- if (!(value instanceof TemplateResult)) {
277
- this.processor?.(initialValue);
278
- }
279
- }
238
+ this.processor = processNodePart(this.markers?.endNode, initialValue, this.markers?.serverSideRendered);
280
239
  }
281
240
 
282
241
  /**
@@ -290,9 +249,7 @@ export class ChildNodePart extends Part {
290
249
  this.updateParts(value);
291
250
  }
292
251
 
293
- if (!(value instanceof TemplateResult)) {
294
- this.processor?.(parsedValue);
295
- }
252
+ this.processor?.(parsedValue);
296
253
  }
297
254
 
298
255
  /**
@@ -304,38 +261,42 @@ export class ChildNodePart extends Part {
304
261
  }
305
262
  if (Array.isArray(value)) {
306
263
  for (let index = 0; index < value.length; index++) {
307
- this.parts[index]?.update(value[index]);
264
+ this.arrayParts[index]?.update(value[index]);
308
265
  }
309
266
  }
310
267
  }
311
268
 
312
269
  /**
313
270
  * @param {TemplateResult | any[] | any} value
314
- * @return {TemplatePart | any[] | any}
271
+ * @return {TemplatePart | Node | string | Array<TemplatePart | Node | string>}
315
272
  */
316
273
  parseValue(value) {
317
274
  if (value instanceof TemplateResult) {
318
275
  if (!this.templatePart) {
319
- const startNode = Array.from(this.childNodes)
320
- .filter((node) => node.nodeType === COMMENT_NODE)
321
- .find((node) => node.data === 'template-part');
322
- this.templatePart = new TemplatePart(startNode, value);
323
- const serverSideRendered = startNode !== undefined;
324
- if (!serverSideRendered) {
325
- // TODO: we should already have the "endNode" from the constructor...
326
- const endNode = Array.from(this.childNodes)
327
- .filter((node) => node.nodeType === COMMENT_NODE)
328
- .find((node) => node.data.includes('/dom-part-'));
276
+ this.templatePart = new TemplatePart(this.markers?.findNestedStartNode('template-part'), value);
277
+ if (!this.markers?.serverSideRendered) {
329
278
  // INFO: this is similar to TemplateResult#237
330
- insertNodesBetweenComments(endNode, this.templatePart.childNodes);
279
+ replaceNodesBetweenComments(this.markers?.endNode, this.templatePart.childNodes);
331
280
  }
281
+ } else if (this.templatePart.strings !== value.strings) {
282
+ // this means we have a different TemplateResult here with maybe different parts also
283
+ this.templatePart = new TemplatePart(undefined, value);
284
+ // INFO: this is similar to TemplateResult#237
285
+ replaceNodesBetweenComments(this.markers?.endNode, this.templatePart.childNodes);
332
286
  }
333
287
  return this.templatePart;
334
288
  }
335
289
  if (Array.isArray(value)) {
336
290
  return this.parseArray(value);
337
291
  }
338
- return value;
292
+ if (value?.nodeType) {
293
+ return value;
294
+ }
295
+ if (typeof value === 'function') {
296
+ return this.parseValue(value());
297
+ }
298
+ // Everything else will be rendered as TextNode
299
+ return value.toString();
339
300
  }
340
301
 
341
302
  /**
@@ -348,28 +309,30 @@ export class ChildNodePart extends Part {
348
309
  for (let index = 0; index < values.length; index++) {
349
310
  let value = values[index];
350
311
 
312
+ // TODO: it would be better to use this.parseValue() here!!!
313
+
351
314
  if (value instanceof TemplateResult) {
352
315
  // TODO this is probably wrong. Should only be taken from parts cache if it has the same type (eg. template vs string, mixed types arrays)
353
- let templatePart = this.parts[index];
316
+ let templatePart = this.arrayParts[index];
354
317
  if (!templatePart) {
355
- const templatePartCommentNodes = this.childNodes?.filter(
318
+ const templatePartCommentNodes = this.markers?.childNodes?.filter(
356
319
  (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
357
320
  );
358
321
  const startNode = templatePartCommentNodes[index];
359
322
  templatePart = new TemplatePart(startNode, value);
360
- this.parts[index] = templatePart;
323
+ this.arrayParts[index] = templatePart;
361
324
  }
362
325
  parsedValues[index] = templatePart;
363
326
  } else if (Array.isArray(value)) {
364
- let childNodePart = this.parts[index];
327
+ let childNodePart = this.arrayParts[index];
365
328
  if (!childNodePart) {
366
329
  // TODO: this seems not correct :( (maybe Template vs DOM Parts !?)
367
- const templatePartCommentNodes = this.childNodes?.filter(
330
+ const templatePartCommentNodes = this.markers?.childNodes?.filter(
368
331
  (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
369
332
  );
370
333
  const startNode = templatePartCommentNodes[index];
371
334
  childNodePart = new ChildNodePart(startNode, value);
372
- this.parts[index] = childNodePart;
335
+ this.arrayParts[index] = childNodePart;
373
336
  }
374
337
  parsedValues[index] = childNodePart;
375
338
  } else {
@@ -0,0 +1,62 @@
1
+ import { COMMENT_NODE } from '../util/DOMHelper.js';
2
+
3
+ export class PartMarkers {
4
+ /** @type {Comment} */
5
+ startNode;
6
+
7
+ /** @type {Comment} */
8
+ endNode;
9
+
10
+ /** @type {Node[]} */
11
+ childNodes = [];
12
+
13
+ /** @type {boolean} */
14
+ serverSideRendered = false;
15
+
16
+ /**
17
+ * @param {Comment} start
18
+ * @param {Comment} end
19
+ * @param {Node[]} childNodes
20
+ * @return {void}
21
+ */
22
+ constructor(start, end, childNodes) {
23
+ this.startNode = start;
24
+ this.endNode = end;
25
+ this.childNodes = childNodes;
26
+
27
+ // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
28
+ if (childNodes.length > 2) {
29
+ this.serverSideRendered = true;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param {string} partType The type of the node to search for. Defaults to 'template-part'.
35
+ * @return {Node|undefined} The first comment node matching the specified type, or undefined if no match is found.
36
+ */
37
+ findNestedStartNode(partType = 'template-part') {
38
+ return this.childNodes.filter((node) => node.nodeType === COMMENT_NODE).find((node) => node.data === partType);
39
+ }
40
+
41
+ /**
42
+ * Creates a new instance of PartMarkers from the given start node.
43
+ * @param {Comment} startNode - The starting comment node that marks the beginning of the section.
44
+ * @return {PartMarkers} A new instance of the PartMarkers class encapsulating the start and end nodes.
45
+ */
46
+ static createFromStartNode(startNode) {
47
+ let endNode;
48
+
49
+ const placeholder = startNode.data;
50
+ const childNodes = [startNode];
51
+ let childNode = startNode.nextSibling;
52
+ while (childNode && childNode.data !== `/${placeholder}`) {
53
+ childNodes.push(childNode);
54
+ childNode = childNode.nextSibling;
55
+ }
56
+
57
+ endNode = childNode;
58
+ childNodes.push(endNode);
59
+
60
+ return new PartMarkers(startNode, endNode, childNodes);
61
+ }
62
+ }
@@ -2,17 +2,21 @@ import { COMMENT_NODE, convertStringToTemplate } from '../util/DOMHelper.js';
2
2
  import { Part } from './Part.js';
3
3
  import { TemplateResult } from './TemplateResult.js';
4
4
  import { AttributePart } from './AttributePart.js';
5
- import { ChildNodePart, processNodePart } from './ChildNodePart.js';
5
+ import { ChildNodePart, getNodesBetweenComments, replaceNodesBetweenComments } from './ChildNodePart.js';
6
6
  import { NodePart } from './NodePart.js';
7
7
  import { RawTextNodePart } from './RawTextNodePart.js';
8
+ import { PartMarkers } from './PartMarkers.js';
8
9
 
9
- /** @type {Map<TemplateStringsArray, Part[]>} */
10
+ /** @type {WeakMap<TemplateStringsArray, Part[]>} */
10
11
  const partsCache = new WeakMap();
11
12
 
12
- /** @type {Map<TemplateStringsArray, DocumentFragment>} */
13
+ /** @type {WeakMap<TemplateStringsArray, DocumentFragment>} */
13
14
  const fragmentsCache = new WeakMap();
14
15
 
15
16
  export class TemplatePart extends Part {
17
+ /** @type {PartMarkers|undefined} */
18
+ markers;
19
+
16
20
  /** @type {Part[]} */
17
21
  parts = [];
18
22
 
@@ -21,9 +25,10 @@ export class TemplatePart extends Part {
21
25
 
22
26
  /** @type {Node[]} */
23
27
  childNodes = [];
28
+ // TODO: there is childNodes here and also in this.markers now...
24
29
 
25
30
  /**
26
- * @param {Node} startNode - the start comment marker node
31
+ * @param {Node|undefined} startNode - the start comment marker node
27
32
  * @param {TemplateResult} value
28
33
  */
29
34
  constructor(startNode, value) {
@@ -33,53 +38,37 @@ export class TemplatePart extends Part {
33
38
 
34
39
  super();
35
40
 
36
- let endNode = undefined;
37
- let serverSideRendered = false;
38
41
  if (startNode) {
39
42
  startNode.__part = this; // add Part to comment node for debugging in the browser
40
-
41
- const placeholder = startNode.data;
42
- const childNodes = [startNode];
43
- let childNode = startNode.nextSibling;
44
- while (childNode && childNode.data !== `/${placeholder}`) {
45
- childNodes.push(childNode);
46
- childNode = childNode.nextSibling;
47
- }
48
-
49
- endNode = childNode;
50
- childNodes.push(endNode);
51
-
52
- // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
53
- if (childNodes.length > 2) {
54
- serverSideRendered = true;
55
- }
56
-
57
- this.childNodes = childNodes;
43
+ this.markers = PartMarkers.createFromStartNode(startNode);
44
+ this.childNodes = this.markers.childNodes;
58
45
  }
59
46
 
60
47
  this.parseValue(value);
61
48
 
62
- if (!serverSideRendered) {
49
+ if (!this.markers?.serverSideRendered) {
63
50
  // TODO: this is causing double diffing because of the this.parseValue() before, ChildNode parts will be constructed + processed
51
+ // TODO: I think that maybe ChildNode Parts work, but Other Parts like Attributes are not set initially otherwise...
64
52
  this.updateParts(value.values);
65
53
  }
66
54
 
67
- endNode = this.childNodes[this.childNodes.length - 1];
68
-
69
- if (endNode) {
70
- this.processor = processNodePart(endNode, this);
71
- }
55
+ // TODO: this is somewhat strange that we are creating this twice...
56
+ this.markers = PartMarkers.createFromStartNode(this.childNodes[0]);
72
57
  }
73
58
 
74
59
  /**
75
- * @param {TemplateResult} value
60
+ * @param {TemplateResult} templateResult
76
61
  */
77
- update(value) {
78
- const shouldUpdateDOM = this.parseValue(value);
62
+ update(templateResult) {
63
+ const shouldUpdateDOM = this.strings !== templateResult.strings;
79
64
  if (shouldUpdateDOM) {
80
- this.processor?.(this);
65
+ this.parseValue(templateResult);
66
+ // TODO: this is a strange way of replacing the childnodes...
67
+ const endNode = this.childNodes[this.childNodes.length - 1];
68
+ // INFO: this is similar to TemplateResult#237
69
+ replaceNodesBetweenComments(this.markers?.endNode, getNodesBetweenComments(endNode));
81
70
  }
82
- this.updateParts(value.values);
71
+ this.updateParts(templateResult.values);
83
72
  }
84
73
 
85
74
  /**
@@ -93,58 +82,44 @@ export class TemplatePart extends Part {
93
82
 
94
83
  /**
95
84
  * @param {TemplateResult} templateResult
96
- * @return {boolean} whether the part needs to be updated in the DOM
97
85
  */
98
86
  parseValue(templateResult) {
99
- if (this.strings !== templateResult.strings) {
100
- let fragment = fragmentsCache.get(templateResult.strings);
101
- if (!fragment) {
102
- fragment = this.parseFragment(templateResult);
103
- fragmentsCache.set(templateResult.strings, fragment);
104
- }
105
- const importedFragment = globalThis.document?.importNode(fragment, true);
106
- this.childNodes = importedFragment.childNodes;
107
-
108
- let parts = partsCache.get(templateResult.strings);
109
- if (!parts) {
110
- parts = templateResult.parseParts(this.childNodes);
111
- partsCache.set(templateResult.strings, parts);
112
- }
113
-
114
- this.parts = parts
115
- .map((part) => {
116
- // We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
117
- part.node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
118
- return part;
119
- })
120
- .map((part, index) => {
121
- if (part.type === 'node') {
122
- return new ChildNodePart(part.node, templateResult.values[index]);
123
- }
124
- if (part.type === 'attribute') {
125
- return new AttributePart(part.node, part.name, part.initialValue);
126
- }
127
- if (part.type === 'raw-text-node') {
128
- return new RawTextNodePart(part.node, part.initialValue);
129
- }
130
- if (part.type === 'directive') {
131
- return new NodePart(part.node, templateResult.values[index]);
132
- }
133
- throw `cannot map part: ${part}`;
134
- });
135
- this.strings = templateResult.strings;
136
- this.childNodes = [...this.childNodes];
137
- return true;
87
+ let fragment = fragmentsCache.get(templateResult.strings);
88
+ if (!fragment) {
89
+ fragment = convertStringToTemplate(templateResult.templateString);
90
+ fragmentsCache.set(templateResult.strings, fragment);
138
91
  }
139
- return false;
140
- }
92
+ const importedFragment = globalThis.document?.importNode(fragment, true);
93
+ this.childNodes = importedFragment.childNodes;
141
94
 
142
- /**
143
- * @param {TemplateResult} templateResult
144
- * @return {DocumentFragment}
145
- */
146
- parseFragment(templateResult) {
147
- const templateString = templateResult.templateString;
148
- return convertStringToTemplate(templateString);
95
+ let parts = partsCache.get(templateResult.strings);
96
+ if (!parts) {
97
+ parts = templateResult.parseParts(this.childNodes);
98
+ partsCache.set(templateResult.strings, parts);
99
+ }
100
+
101
+ this.parts = parts
102
+ .map((part) => {
103
+ // We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
104
+ part.node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
105
+ return part;
106
+ })
107
+ .map((part, index) => {
108
+ if (part.type === 'node') {
109
+ return new ChildNodePart(part.node, templateResult.values[index]);
110
+ }
111
+ if (part.type === 'attribute') {
112
+ return new AttributePart(part.node, part.name, part.initialValue);
113
+ }
114
+ if (part.type === 'raw-text-node') {
115
+ return new RawTextNodePart(part.node, 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
+ this.childNodes = [...this.childNodes];
149
124
  }
150
125
  }