@webtides/element-js 1.2.12 → 1.3.1

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.
@@ -158,27 +158,36 @@ const attributeString = spreadAttributes(attrs);
158
158
 
159
159
  ##### Writing custom directives
160
160
 
161
- Directives in `element-js` are just functions. They accept parameters and return values that manipulate or produce templates or strings.
161
+ The built-in directives above are plain functions that return a value for an attribute or a child position. A directive that works on the element itself sits inside the opening tag, between the attributes. It is a class that extends `Directive`, wrapped with `defineDirective()`:
162
162
 
163
- To write a custom directive:
164
-
165
- Define a Function: This function will describe what your directive does. The function should return either a template, string, or another function based on its parameters.
163
+ - `update(...values)` runs in the browser on every render, `this.node` is the element the directive sits on.
164
+ - `stringify(...values)` runs when the template is rendered to a string, for example on the server, and returns what is written into the opening tag.
166
165
 
167
166
  ```javascript
168
- const visibility = (isVisible) => {
169
- return isVisible ? '' : 'style="display: none;"';
170
- };
167
+ import { Directive, defineDirective } from '@webtides/element-js';
168
+
169
+ class HighlightDirective extends Directive {
170
+ update(active) {
171
+ this.node.toggleAttribute('data-highlight', active);
172
+ }
173
+
174
+ stringify(active) {
175
+ return active ? 'data-highlight' : '';
176
+ }
177
+ }
178
+
179
+ const highlight = defineDirective(HighlightDirective);
171
180
  ```
172
181
 
173
- Use your custom directive in your templates.
182
+ `defineDirective()` returns the function to call in templates, its arguments are passed on to `update()` and `stringify()`:
174
183
 
175
184
  ```javascript
176
185
  function template() {
177
- return html` <div ${visibility(false)}>This div will be hidden.</div> `;
186
+ return html` <div ${highlight(this.active)}>Highlighted while active.</div> `;
178
187
  }
179
188
  ```
180
189
 
181
- Remember, the power of directives is in their simplicity. They allow you to encapsulate logic in a function and reuse that logic across multiple templates.
190
+ A plain function at this position throws, the renderer only accepts a directive created with `defineDirective()`. The `about` tab of `examples/showcase/` uses this directive.
182
191
 
183
192
  #### Slots
184
193
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.2.12",
3
+ "version": "1.3.1",
4
4
  "module": "index.js",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -36,12 +36,12 @@
36
36
  "element-js",
37
37
  "shadow-dom"
38
38
  ],
39
- "dependencies": {},
40
39
  "devDependencies": {
41
40
  "@babel/eslint-parser": "^7.23.3",
42
41
  "@open-wc/testing": "^4.0.0",
43
42
  "@web/dev-server": "^0.4.6",
44
43
  "@web/test-runner": "^0.19.0",
44
+ "@web/test-runner-commands": "^0.9.0",
45
45
  "babel-eslint": "^10.1.0",
46
46
  "eslint": "8.55.0",
47
47
  "eslint-config-prettier": "^9.1.0",
@@ -60,6 +60,9 @@
60
60
  "scripts": {
61
61
  "lint": "eslint index.js 'src/**/*.js'",
62
62
  "test": "web-test-runner test/**/*.test.{html,js} --node-resolve",
63
+ "bench": "node benchmark/run.mjs",
64
+ "bench:compare": "node benchmark/run.mjs --base main",
65
+ "bench:ui": "web-dev-server --root-dir . --open /benchmark/",
63
66
  "test:coverage": "web-test-runner test/**/*.test.{html,js} --node-resolve --coverage",
64
67
  "test:watch": "web-test-runner test/**/*.test.{html,js} --node-resolve --watch",
65
68
  "start": "web-dev-server --config web-dev-server.config.mjs"
@@ -55,10 +55,12 @@ class TemplateElement extends StyledElement {
55
55
  * This will attach a shadow DOM if the element is supposed to render in shadow DOM.
56
56
  */
57
57
  renderTemplate() {
58
- const firstRender = this._options.shadowRender && !this.shadowRoot;
59
- if (firstRender) {
58
+ if (this._options.shadowRender && !this.shadowRoot) {
60
59
  this.attachShadow({ mode: 'open' });
61
- // init external stylesheets
60
+ }
61
+ // the first render adopts the style sheets, also into a declarative shadow root that the parser attached
62
+ if (this._options.shadowRender && !this._stylesAdopted) {
63
+ this._stylesAdopted = true;
62
64
  this.initStyleAdoption();
63
65
  }
64
66
 
@@ -1,16 +1,30 @@
1
1
  import { Part } from './Part.js';
2
2
 
3
+ // marks the first update: it compares with the attribute in the DOM once, so a fresh clone, which carries the
4
+ // placeholder, gets written and server rendered markup that fits already is left alone
5
+ const NOT_SET = Symbol('not set');
6
+
3
7
  /**
4
8
  * @param {Element} node
5
9
  * @param {String} name
6
- * @param {Boolean} oldValue
7
10
  * @return {(function(*): void)|*}
8
11
  */
9
- const processBooleanAttribute = (node, name, oldValue) => {
12
+ const processBooleanAttribute = (node, name) => {
13
+ let oldValue = NOT_SET;
10
14
  return (newValue) => {
11
15
  const value = !!newValue?.valueOf() && newValue !== 'false';
16
+ if (oldValue === NOT_SET && node.getAttribute(name) === (value ? '' : null)) {
17
+ oldValue = value;
18
+ return;
19
+ }
12
20
  if (oldValue !== value) {
13
- node.toggleAttribute(name, (oldValue = !!value));
21
+ oldValue = value;
22
+ // not toggleAttribute(): server rendered markup may carry `name="false"`, true has to become `name=""`
23
+ if (value) {
24
+ node.setAttribute(name, '');
25
+ } else {
26
+ node.removeAttribute(name);
27
+ }
14
28
  }
15
29
  };
16
30
  };
@@ -49,9 +63,13 @@ const processEventAttribute = (node, name) => {
49
63
  * @return {(function(*): void)|*}
50
64
  */
51
65
  const processAttribute = (node, name) => {
52
- let oldValue;
66
+ let oldValue = NOT_SET;
53
67
  return (newValue) => {
54
68
  const value = newValue?.valueOf();
69
+ if (oldValue === NOT_SET && node.getAttribute(name) === (value == null ? null : String(value))) {
70
+ oldValue = value;
71
+ return;
72
+ }
55
73
  if (oldValue !== value) {
56
74
  oldValue = value;
57
75
  if (value == null) {
@@ -74,7 +92,7 @@ const attributePartsCache = new WeakMap();
74
92
  export const processAttributePart = (node, name) => {
75
93
  // boolean attribute: ?boolean=${...}
76
94
  if (name.startsWith('?')) {
77
- return processBooleanAttribute(node, name.slice(1), false);
95
+ return processBooleanAttribute(node, name.slice(1));
78
96
  }
79
97
 
80
98
  // property attribute: .property=${...}
@@ -82,11 +100,23 @@ export const processAttributePart = (node, name) => {
82
100
  return processPropertyAttribute(node, name.slice(1));
83
101
  }
84
102
 
85
- // event attribute: @event=${...} || "old school" event attribute: onevent=${...}
86
- if (name.startsWith('@') || name.startsWith('on')) {
103
+ // event attribute: @event=${...}
104
+ if (name.startsWith('@')) {
87
105
  return processEventAttribute(node, name);
88
106
  }
89
107
 
108
+ // "old school" event attribute: onevent=${...}. A function is the listener, any other value is written as an
109
+ // attribute: `on="${state}"` or `once="${flag}"` are attributes of the element that happen to start with "on"
110
+ if (name.startsWith('on')) {
111
+ const listener = processEventAttribute(node, name);
112
+ const attribute = processAttribute(node, name);
113
+ return (value) => {
114
+ const isListener = typeof value === 'function';
115
+ listener(isListener ? value : undefined);
116
+ attribute(isListener ? undefined : value);
117
+ };
118
+ }
119
+
90
120
  // normal "string" attribute: attribute=${...}
91
121
  return processAttribute(node, name);
92
122
  };
@@ -1,4 +1,4 @@
1
- import { COMMENT_NODE, ELEMENT_NODE } from '../util/DOMHelper.js';
1
+ import { COMMENT_NODE, TEXT_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';
@@ -69,42 +69,54 @@ export const getNodesBetweenComments = (commentNode, includeComments = false) =>
69
69
  };
70
70
 
71
71
  /**
72
- * @param {Node[]} newChildNodes
73
- * @param {Comment} anchorNode
72
+ * A node that is not in the DOM yet takes over an equal node that is in place: a fresh text node leaves the text node
73
+ * that is there alone. The nodes of a template clone still sit in their fragment and never take over, they replace
74
+ * what the server rendered in their place.
75
+ * @param {Node} node
76
+ * @param {Node} newChildNode
77
+ * @return {boolean}
74
78
  */
75
- const diffChildNodes = function (newChildNodes, anchorNode) {
76
- const parentNode = anchorNode.parentNode;
77
- const oldChildNodes = getNodesBetweenComments(anchorNode);
78
-
79
- const length = Math.max(oldChildNodes.length, newChildNodes.length);
80
- // Diff each node in the child node lists
81
- for (let index = 0; index < length; index++) {
82
- let oldChildNode = oldChildNodes[index];
83
- let newChildNode = newChildNodes[index];
79
+ const takesOver = (node, newChildNode) => !newChildNode.parentNode && node.isEqualNode(newChildNode);
84
80
 
85
- if (newChildNode && !newChildNode.nodeType) {
81
+ /**
82
+ * Brings the nodes between the markers in line with newChildNodes, in that order. Walks the live DOM with a cursor:
83
+ * a node that is at the cursor already stays, stale nodes at the cursor are removed, anything else is inserted in
84
+ * front of the cursor, which moves a node that is in the DOM further back, for example a hydrated template behind a
85
+ * server rendered text. Nodes that are in place are never touched, so focus and element lifecycles survive.
86
+ * @param {Node[]} newChildNodes
87
+ * @param {Comment} startNode - the start marker
88
+ * @param {Comment} endNode - the end marker
89
+ */
90
+ const diffChildNodes = (newChildNodes, startNode, endNode) => {
91
+ const parentNode = endNode.parentNode;
92
+ // the set of new nodes tells a stale node from one that is needed further on. It is only built when the nodes
93
+ // deviate from what is there, a list whose nodes are all in place never pays for it
94
+ let keep;
95
+ let current = startNode.nextSibling;
96
+ for (let index = 0; index < newChildNodes.length; index++) {
97
+ const newChildNode = newChildNodes[index];
98
+ if (!newChildNode?.nodeType) {
86
99
  throw new Error('ChildNodePart: newChildNode is not a node');
87
100
  }
88
-
89
- // If the DOM node doesn't exist, append/copy the new node
90
- if (oldChildNode === undefined) {
91
- parentNode.insertBefore(newChildNode, anchorNode);
92
- continue;
93
- }
94
-
95
- // If the new node doesn't exist, remove the node in the DOM
96
- if (newChildNode === undefined) {
97
- parentNode.removeChild(oldChildNode);
98
- continue;
101
+ if (current !== newChildNode && current !== endNode) {
102
+ keep ??= new Set(newChildNodes);
103
+ while (current !== endNode && !keep.has(current) && !takesOver(current, newChildNode)) {
104
+ const next = current.nextSibling;
105
+ parentNode.removeChild(current);
106
+ current = next;
107
+ }
99
108
  }
100
-
101
- // If DOM node is equal to the new node, don't do anything in the DOM
102
- if (oldChildNode === newChildNode || oldChildNode.isEqualNode(newChildNode)) {
103
- continue;
109
+ if (current === newChildNode || (current !== endNode && !keep.has(current))) {
110
+ // in place already, or a stale node the new one takes over
111
+ current = current.nextSibling;
112
+ } else {
113
+ parentNode.insertBefore(newChildNode, current);
104
114
  }
105
-
106
- // Everything else is somehow different and can be replaced
107
- parentNode.replaceChild(newChildNode, oldChildNode);
115
+ }
116
+ while (current !== endNode) {
117
+ const next = current.nextSibling;
118
+ parentNode.removeChild(current);
119
+ current = next;
108
120
  }
109
121
  };
110
122
 
@@ -116,16 +128,13 @@ function getType(value) {
116
128
  return 'text';
117
129
  }
118
130
 
119
- // TODO: handle funcitons?! Should be handled in parseArray!!!
131
+ // the parsed items of an array as the child nodes to diff: template parts contribute their nodes, primitives become
132
+ // text nodes, DOM nodes pass through
120
133
  const unwrapArray = (nodes) => {
121
- // we have to unwrap any complex objects inside the array so that we can diff arrays of childNodes
122
134
  return nodes.flatMap((value) => {
123
135
  if (value instanceof TemplatePart) {
124
136
  return value.childNodes;
125
137
  }
126
- if (Array.isArray(value)) {
127
- return unwrapArray(value);
128
- }
129
138
  if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
130
139
  return document.createTextNode(value);
131
140
  }
@@ -134,76 +143,72 @@ const unwrapArray = (nodes) => {
134
143
  };
135
144
 
136
145
  /**
137
- * @param {Comment} comment
138
- * @param {TemplatePart | any[] | any} initialValue
139
- * @param {boolean} serverSideRendered
140
- * @return {(function(*): void)|*}
146
+ * Array items get the same treatment as a single value: functions are resolved completely, arrays nested in the array
147
+ * are flattened and null or undefined items are dropped, which is the list the server renders. An array that needs
148
+ * none of it is returned as is.
149
+ * @param {any[]} values
150
+ * @return {any[]}
151
+ */
152
+ const resolveArrayItems = (values) => {
153
+ if (!values.some((value) => typeof value === 'function' || Array.isArray(value) || value == null)) {
154
+ return values;
155
+ }
156
+ return values.flatMap((item) => {
157
+ let value = item;
158
+ while (typeof value === 'function') value = value();
159
+ if (Array.isArray(value)) return resolveArrayItems(value);
160
+ return value == null ? [] : [value];
161
+ });
162
+ };
163
+
164
+ /**
165
+ * Creates the processor that writes a parsed value between the markers of a child node part.
166
+ * @param {PartMarkers} markers - the start and end marker of the part
167
+ * @return {(function(TemplatePart | any[] | any, boolean=): void)}
141
168
  */
142
- export const processNodePart = (comment, initialValue, serverSideRendered = false) => {
169
+ export const processNodePart = ({ startNode, endNode }) => {
143
170
  let oldValueType = 'empty';
144
171
  let oldValue = undefined;
145
- // 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.
146
- // TODO: when server side rendered, we need to get the text node from the DOM no?!
172
+ // string values are rendered into one text node that is reused across updates. Hydration takes over the server
173
+ // rendered text node as that node
147
174
  let cachedTextNode = globalThis.document?.createTextNode('');
148
175
 
149
- if (serverSideRendered) {
150
- oldValueType = getType(initialValue);
151
- oldValue = initialValue instanceof TemplatePart ? initialValue.strings : initialValue;
152
- }
153
-
154
- const processNodeValue = (newValue) => {
155
- let newValueType = getType(newValue);
176
+ return (newValue, hydrate = false) => {
177
+ const newValueType = getType(newValue);
156
178
 
157
179
  if (oldValueType === newValueType && oldValue === newValue) return;
158
180
 
159
- if (oldValueType !== newValueType) {
160
- if (newValueType === 'empty') {
161
- removeNodesBetweenComments(comment);
181
+ if (newValueType === 'empty') {
182
+ removeNodesBetweenComments(endNode);
183
+ } else if (newValueType === 'text') {
184
+ if (oldValueType !== 'text') {
185
+ const serverTextNode = startNode.nextSibling;
186
+ if (hydrate && serverTextNode.nodeType === TEXT_NODE && serverTextNode.nextSibling === endNode) {
187
+ cachedTextNode = serverTextNode;
188
+ } else {
189
+ replaceNodesBetweenComments(endNode, [cachedTextNode]);
190
+ }
191
+ }
192
+ if (cachedTextNode.data !== String(newValue)) cachedTextNode.data = newValue;
193
+ } else if (newValueType === 'templatePart') {
194
+ // a template part bound to server rendered DOM (hydration) is in place already and is left alone
195
+ if (newValue.childNodes[0].parentNode !== endNode.parentNode) {
196
+ replaceNodesBetweenComments(endNode, newValue.childNodes);
197
+ }
198
+ } else {
199
+ const nodes = newValueType === 'array' ? unwrapArray(newValue) : [newValue];
200
+ // a type change replaces what is there. Server rendered nodes (hydration) go through the diff instead,
201
+ // nodes that are in place already, for example a focused input, stay untouched
202
+ if (oldValueType !== newValueType && !hydrate) {
203
+ replaceNodesBetweenComments(endNode, nodes);
162
204
  } else {
163
- const getNodesForType = (value, type) => {
164
- if (type === 'templatePart') return value.childNodes;
165
- if (type === 'array') return unwrapArray(value);
166
- if (type === 'node') return [value];
167
-
168
- // Every else falls back to rendering a text node
169
- cachedTextNode.data = value;
170
- return [cachedTextNode];
171
- };
172
- replaceNodesBetweenComments(comment, getNodesForType(newValue, newValueType));
205
+ diffChildNodes(nodes, startNode, endNode);
173
206
  }
174
-
175
- oldValueType = newValueType;
176
- oldValue = newValue;
177
- return;
178
- }
179
-
180
- if (newValueType === 'text') {
181
- cachedTextNode.data = newValue;
182
-
183
- oldValue = newValue;
184
- return;
185
- }
186
-
187
- if (newValueType === 'array') {
188
- diffChildNodes(unwrapArray(newValue), comment);
189
-
190
- oldValue = newValue;
191
- return;
192
207
  }
193
208
 
194
- if (newValueType === 'node') {
195
- diffChildNodes([newValue], comment);
196
-
197
- oldValue = newValue;
198
- }
209
+ oldValueType = newValueType;
210
+ oldValue = newValue;
199
211
  };
200
-
201
- if (!serverSideRendered) {
202
- // initial rendering
203
- processNodeValue(initialValue);
204
- }
205
-
206
- return processNodeValue;
207
212
  };
208
213
 
209
214
  /**
@@ -216,17 +221,16 @@ export class ChildNodePart extends Part {
216
221
  /** @type {TemplatePart|undefined} */
217
222
  templatePart;
218
223
 
219
- /** @type {Part[]} */
224
+ /** @type {TemplatePart[]} indexed like the items of an array value, only template items have a part */
220
225
  arrayParts = [];
221
226
 
222
227
  /**
223
228
  * @param {Node} startNode - the start comment marker node
224
- * @param {any | any[]} value
225
229
  */
226
- constructor(startNode, value) {
230
+ constructor(startNode) {
227
231
  if (!startNode || startNode?.nodeType !== COMMENT_NODE) {
228
232
  throw new Error(
229
- startNode ? 'ChildNodePart: startNode is undefined' : 'ChildNodePart: startNode is not a comment node',
233
+ startNode ? 'ChildNodePart: startNode is not a comment node' : 'ChildNodePart: startNode is undefined',
230
234
  );
231
235
  }
232
236
 
@@ -234,70 +238,63 @@ export class ChildNodePart extends Part {
234
238
 
235
239
  startNode.__part = this; // add Part to comment node for debugging in the browser
236
240
  this.markers = PartMarkers.createFromStartNode(startNode);
237
-
238
- // value can become array | TemplatePart | any
239
- const initialValue = this.parseValue(value);
240
-
241
- this.processor = processNodePart(this.markers?.endNode, initialValue, this.markers?.serverSideRendered);
241
+ this.processor = processNodePart(this.markers);
242
242
  }
243
243
 
244
244
  /**
245
- * @param {any} value
246
- * @return {*}
245
+ * @param {TemplateResult | any[] | any} value
246
+ * @param {boolean} [hydrate] - bind nested template parts to the server rendered markers instead of cloning
247
247
  */
248
- update(value) {
249
- const parsedValue = this.parseValue(value);
250
-
251
- if (value instanceof TemplateResult || Array.isArray(value)) {
252
- this.updateParts(value);
253
- }
254
-
255
- this.processor?.(parsedValue);
248
+ update(value, hydrate = false) {
249
+ // functions are resolved completely and array items are normalized, parsing and updating have to work on the
250
+ // same value
251
+ let resolvedValue = value;
252
+ while (typeof resolvedValue === 'function') resolvedValue = resolvedValue();
253
+ if (Array.isArray(resolvedValue)) resolvedValue = resolveArrayItems(resolvedValue);
254
+ const parsedValue = this.parseValue(resolvedValue, hydrate);
255
+ this.updateParts(resolvedValue, hydrate);
256
+ this.processor(parsedValue, hydrate);
256
257
  }
257
258
 
258
259
  /**
259
- * @param {TemplateResult | any[]} value
260
+ * @param {TemplateResult | any[] | any} value
261
+ * @param {boolean} [hydrate]
260
262
  */
261
- updateParts(value) {
263
+ updateParts(value, hydrate = false) {
262
264
  if (value instanceof TemplateResult) {
263
- this.templatePart.update(value);
265
+ this.templatePart.update(value, hydrate);
264
266
  }
265
267
  if (Array.isArray(value)) {
266
268
  for (let index = 0; index < value.length; index++) {
267
- this.arrayParts[index]?.update(value[index]);
269
+ if (value[index] instanceof TemplateResult) this.arrayParts[index].update(value[index], hydrate);
268
270
  }
269
271
  }
270
272
  }
271
273
 
272
274
  /**
275
+ * Creates the nested parts for a value. When hydrating, nested template parts are bound to the server rendered
276
+ * template-part markers between this part's markers.
273
277
  * @param {TemplateResult | any[] | any} value
278
+ * @param {boolean} hydrate
274
279
  * @return {TemplatePart | Node | string | Array<TemplatePart | Node | string>}
275
280
  */
276
- parseValue(value) {
281
+ parseValue(value, hydrate = false) {
277
282
  if (value instanceof TemplateResult) {
278
283
  if (!this.templatePart) {
279
- this.templatePart = new TemplatePart(this.markers?.findNestedStartNode('template-part'), value);
280
- if (!this.markers?.serverSideRendered) {
281
- // INFO: this is similar to TemplateResult#237
282
- replaceNodesBetweenComments(this.markers?.endNode, this.templatePart.childNodes);
283
- }
284
+ const startNode = hydrate ? this.markers.findNestedStartNode('template-part') : undefined;
285
+ this.templatePart = new TemplatePart(startNode, value);
284
286
  } else if (this.templatePart.strings !== value.strings) {
285
287
  // this means we have a different TemplateResult here with maybe different parts also
286
288
  this.templatePart = new TemplatePart(undefined, value);
287
- // INFO: this is similar to TemplateResult#237
288
- replaceNodesBetweenComments(this.markers?.endNode, this.templatePart.childNodes);
289
289
  }
290
290
  return this.templatePart;
291
291
  }
292
292
  if (Array.isArray(value)) {
293
- return this.parseArray(value);
293
+ return this.parseArray(value, hydrate);
294
294
  }
295
295
  if (value?.nodeType) {
296
296
  return value;
297
297
  }
298
- if (typeof value === 'function') {
299
- return this.parseValue(value());
300
- }
301
298
  if (value === null || value === undefined) {
302
299
  return value;
303
300
  }
@@ -306,44 +303,32 @@ export class ChildNodePart extends Part {
306
303
  }
307
304
 
308
305
  /**
309
- * Nested TemplateResults values need to be unrolled in order for update functions to be able to process them
306
+ * Creates the template parts for the template items of an array, indexed like the items. A different template at
307
+ * an index gets a new part, the nodes of the part that was there go away with the diff. Other items (text, nodes)
308
+ * pass through, functions and nested arrays are resolved before, see update().
310
309
  * @param {any[]} values
311
- * @return {any[]}
310
+ * @param {boolean} hydrate
311
+ * @return {Array<TemplatePart | Node | string>}
312
312
  */
313
- parseArray(values) {
313
+ parseArray(values, hydrate = false) {
314
314
  const parsedValues = [];
315
- for (let index = 0; index < values.length; index++) {
316
- let value = values[index];
317
-
318
- // TODO: it would be better to use this.parseValue() here!!!
315
+ // the server rendered template-part markers directly between this part's markers, one per nested template.
316
+ // Other values (text, nodes) have no marker, so the templates are counted separately
317
+ const nestedStartNodes = hydrate ? this.markers.findNestedStartNodes('template-part') : [];
318
+ let nestedIndex = 0;
319
319
 
320
+ for (let index = 0; index < values.length; index++) {
321
+ const value = values[index];
320
322
  if (value instanceof TemplateResult) {
321
- // 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)
322
323
  let templatePart = this.arrayParts[index];
323
- if (!templatePart) {
324
- const templatePartCommentNodes = this.markers?.childNodes?.filter(
325
- (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
326
- );
327
- const startNode = templatePartCommentNodes[index];
328
- templatePart = new TemplatePart(startNode, value);
324
+ if (!templatePart || templatePart.strings !== value.strings) {
325
+ templatePart = new TemplatePart(nestedStartNodes[nestedIndex], value);
329
326
  this.arrayParts[index] = templatePart;
330
327
  }
328
+ nestedIndex++;
331
329
  parsedValues[index] = templatePart;
332
- } else if (Array.isArray(value)) {
333
- let childNodePart = this.arrayParts[index];
334
- if (!childNodePart) {
335
- // TODO: this seems not correct :( (maybe Template vs DOM Parts !?)
336
- const templatePartCommentNodes = this.markers?.childNodes?.filter(
337
- (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
338
- );
339
- const startNode = templatePartCommentNodes[index];
340
- childNodePart = new ChildNodePart(startNode, value);
341
- this.arrayParts[index] = childNodePart;
342
- }
343
- parsedValues[index] = childNodePart;
344
330
  } else {
345
- // TODO: what if the value from the function is a TemplatePart or an array?! Should we do this first thing?!
346
- parsedValues[index] = typeof value === 'function' ? value() : value;
331
+ parsedValues[index] = value;
347
332
  }
348
333
  }
349
334
 
@@ -5,8 +5,10 @@ export class Part {
5
5
  processor = undefined;
6
6
 
7
7
  /**
8
+ * Applies a value to the DOM.
8
9
  * @abstract
9
10
  * @param {TemplateResult | any[] | any} value
11
+ * @param {boolean} [hydrate] - first update of server rendered DOM, nested parts bind to the server rendered markers
10
12
  */
11
- update(value) {}
13
+ update(value, hydrate = false) {}
12
14
  }