@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 +1 -1
- package/src/dom-parts/ChildNodePart.js +190 -227
- package/src/dom-parts/PartMarkers.js +62 -0
- package/src/dom-parts/TemplatePart.js +59 -84
package/package.json
CHANGED
|
@@ -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 {
|
|
8
|
-
* @param {
|
|
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 {
|
|
18
|
+
* @param {Comment} commentNode
|
|
19
|
+
* @param {Node[]} nodes
|
|
18
20
|
*/
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 {
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {Node[]} templateChildNodes
|
|
35
|
-
* @param {Node} anchorNode
|
|
39
|
+
* @param {Comment} commentNode
|
|
40
|
+
* @param {boolean} includeComments
|
|
36
41
|
* @return {Node[]}
|
|
37
42
|
*/
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
let
|
|
79
|
+
let oldChildNode = oldChildNodes[index];
|
|
80
|
+
let newChildNode = newChildNodes[index];
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
132
|
-
let oldValue =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
parts = [];
|
|
220
|
-
|
|
221
|
-
// TODO: this is only for TemplatePart values
|
|
222
|
-
templatePart = undefined;
|
|
210
|
+
/** @type {PartMarkers|undefined} */
|
|
211
|
+
markers;
|
|
223
212
|
|
|
224
|
-
|
|
213
|
+
/** @type {TemplatePart|undefined} */
|
|
214
|
+
templatePart;
|
|
225
215
|
|
|
226
|
-
/** @type {
|
|
227
|
-
|
|
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
|
|
235
|
-
throw new Error(
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 |
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
323
|
+
this.arrayParts[index] = templatePart;
|
|
361
324
|
}
|
|
362
325
|
parsedValues[index] = templatePart;
|
|
363
326
|
} else if (Array.isArray(value)) {
|
|
364
|
-
let childNodePart = this.
|
|
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.
|
|
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,
|
|
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 {
|
|
10
|
+
/** @type {WeakMap<TemplateStringsArray, Part[]>} */
|
|
10
11
|
const partsCache = new WeakMap();
|
|
11
12
|
|
|
12
|
-
/** @type {
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
60
|
+
* @param {TemplateResult} templateResult
|
|
76
61
|
*/
|
|
77
|
-
update(
|
|
78
|
-
const shouldUpdateDOM = this.
|
|
62
|
+
update(templateResult) {
|
|
63
|
+
const shouldUpdateDOM = this.strings !== templateResult.strings;
|
|
79
64
|
if (shouldUpdateDOM) {
|
|
80
|
-
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(
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
140
|
-
|
|
92
|
+
const importedFragment = globalThis.document?.importNode(fragment, true);
|
|
93
|
+
this.childNodes = importedFragment.childNodes;
|
|
141
94
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
}
|