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