@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 +1 -1
- package/src/dom-parts/ChildNodePart.js +200 -217
- package/src/dom-parts/PartMarkers.js +62 -0
- package/src/dom-parts/TemplatePart.js +62 -86
package/package.json
CHANGED
|
@@ -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 {
|
|
8
|
+
* @param {Comment} commentNode
|
|
9
|
+
* @param {Node[]} nodes
|
|
8
10
|
*/
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 {
|
|
23
|
-
* @param {
|
|
24
|
-
* @param {Node[]} templateChildNodes
|
|
25
|
-
* @param {Node} anchorNode
|
|
39
|
+
* @param {Comment} commentNode
|
|
40
|
+
* @param {boolean} includeComments
|
|
26
41
|
* @return {Node[]}
|
|
27
42
|
*/
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
127
|
-
let oldValue =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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 {
|
|
200
|
-
|
|
210
|
+
/** @type {PartMarkers|undefined} */
|
|
211
|
+
markers;
|
|
201
212
|
|
|
202
|
-
/** @type {
|
|
203
|
-
|
|
213
|
+
/** @type {TemplatePart|undefined} */
|
|
214
|
+
templatePart;
|
|
204
215
|
|
|
205
|
-
// TODO: this is only for array values
|
|
206
216
|
/** @type {Part[]} */
|
|
207
|
-
|
|
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
|
|
223
|
-
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
|
+
);
|
|
224
228
|
}
|
|
225
229
|
|
|
226
230
|
super();
|
|
227
231
|
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 |
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
323
|
+
this.arrayParts[index] = templatePart;
|
|
342
324
|
}
|
|
343
325
|
parsedValues[index] = templatePart;
|
|
344
326
|
} else if (Array.isArray(value)) {
|
|
345
|
-
let childNodePart = this.
|
|
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.
|
|
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,
|
|
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,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
|
-
|
|
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
|
|
70
|
-
this.
|
|
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}
|
|
60
|
+
* @param {TemplateResult} templateResult
|
|
79
61
|
*/
|
|
80
|
-
update(
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
139
|
-
|
|
92
|
+
const importedFragment = globalThis.document?.importNode(fragment, true);
|
|
93
|
+
this.childNodes = importedFragment.childNodes;
|
|
140
94
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
}
|