@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.
- package/docs/features/templates.md +19 -10
- package/package.json +5 -2
- package/src/TemplateElement.js +5 -3
- package/src/dom-parts/AttributePart.js +37 -7
- package/src/dom-parts/ChildNodePart.js +141 -156
- package/src/dom-parts/Part.js +3 -1
- package/src/dom-parts/PartMarkers.js +84 -26
- package/src/dom-parts/TemplatePart.js +160 -69
- package/src/dom-parts/TemplateResult.js +71 -84
- package/src/dom-parts/directives.js +11 -1
- package/src/util/GlobalStylesStore.js +5 -6
|
@@ -158,27 +158,36 @@ const attributeString = spreadAttributes(attrs);
|
|
|
158
158
|
|
|
159
159
|
##### Writing custom directives
|
|
160
160
|
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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 ${
|
|
186
|
+
return html` <div ${highlight(this.active)}>Highlighted while active.</div> `;
|
|
178
187
|
}
|
|
179
188
|
```
|
|
180
189
|
|
|
181
|
-
|
|
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.
|
|
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"
|
package/src/TemplateElement.js
CHANGED
|
@@ -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
|
-
|
|
59
|
-
if (firstRender) {
|
|
58
|
+
if (this._options.shadowRender && !this.shadowRoot) {
|
|
60
59
|
this.attachShadow({ mode: 'open' });
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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=${...}
|
|
86
|
-
if (name.startsWith('@')
|
|
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,
|
|
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
|
-
*
|
|
73
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
//
|
|
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
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* @
|
|
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 = (
|
|
169
|
+
export const processNodePart = ({ startNode, endNode }) => {
|
|
143
170
|
let oldValueType = 'empty';
|
|
144
171
|
let oldValue = undefined;
|
|
145
|
-
//
|
|
146
|
-
//
|
|
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
|
-
|
|
150
|
-
|
|
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 (
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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 {
|
|
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
|
|
230
|
+
constructor(startNode) {
|
|
227
231
|
if (!startNode || startNode?.nodeType !== COMMENT_NODE) {
|
|
228
232
|
throw new Error(
|
|
229
|
-
startNode ? 'ChildNodePart: startNode is
|
|
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
|
-
* @
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.
|
|
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]
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
parsedValues[index] = typeof value === 'function' ? value() : value;
|
|
331
|
+
parsedValues[index] = value;
|
|
347
332
|
}
|
|
348
333
|
}
|
|
349
334
|
|
package/src/dom-parts/Part.js
CHANGED
|
@@ -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
|
}
|