@webtides/element-js 0.7.4 → 1.0.0-alpha.10
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/README.md +2 -2
- package/docs/README.md +174 -0
- package/docs/concepts/classes.md +167 -0
- package/docs/concepts/hierarchy.md +61 -0
- package/docs/concepts/lifecycle.md +45 -0
- package/docs/concepts/reactivity.md +14 -0
- package/docs/features/computed-properties.md +28 -0
- package/docs/features/dependency-injection-context-protocol.md +132 -0
- package/docs/features/events.md +155 -0
- package/docs/features/global-styles.md +131 -0
- package/docs/features/methods.md +34 -0
- package/docs/features/properties.md +56 -0
- package/docs/features/refs.md +47 -0
- package/docs/features/renderless.md +70 -0
- package/docs/features/store.md +143 -0
- package/docs/features/styles.md +169 -0
- package/docs/features/templates.md +308 -0
- package/docs/features/watch.md +54 -0
- package/docs/getting-started/upgrade-to-v1.md +243 -0
- package/docs/guide/guide.md +112 -0
- package/index.js +9 -2
- package/package.json +19 -27
- package/src/BaseElement.js +176 -86
- package/src/StyledElement.js +66 -55
- package/src/TemplateElement.js +34 -8
- package/src/dom-parts/AttributePart.js +167 -0
- package/src/dom-parts/ChildNodePart.js +329 -0
- package/src/dom-parts/NodePart.js +38 -0
- package/src/dom-parts/Part.js +12 -0
- package/src/dom-parts/TemplatePart.js +134 -0
- package/src/dom-parts/TemplateResult.js +342 -0
- package/src/dom-parts/directives.js +111 -0
- package/src/dom-parts/html.js +12 -0
- package/src/dom-parts/render.js +10 -0
- package/src/util/AttributeParser.js +62 -29
- package/src/util/DOMHelper.js +253 -5
- package/src/util/Directive.js +42 -0
- package/src/util/GlobalStylesStore.js +66 -0
- package/src/util/SerializeStateHelper.js +103 -0
- package/src/util/Store.js +92 -9
- package/src/util/defineElement.js +2 -2
- package/src/util/toString.js +2 -2
- package/CHANGELOG.md +0 -154
- package/dist/index.js +0 -1673
- package/dist/index.js.map +0 -7
- package/src/renderer/vanilla/TemplateElement.js +0 -50
- package/src/renderer/vanilla/index.js +0 -18
- package/src/renderer/vanilla/util/directives.js +0 -78
- package/src/renderer/vanilla/util/html.js +0 -65
- package/src/renderer/vanilla/util/render.js +0 -149
- package/src/util/i18n.js +0 -20
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Part } from './Part.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Element} node
|
|
5
|
+
* @param {String} name
|
|
6
|
+
* @param {Boolean} oldValue
|
|
7
|
+
* @return {(function(*): void)|*}
|
|
8
|
+
*/
|
|
9
|
+
const processBooleanAttribute = (node, name, oldValue) => {
|
|
10
|
+
// TODO: It would be better if the ?boolean= attribute would not be there in the first place...
|
|
11
|
+
node.removeAttribute(`?${name}`);
|
|
12
|
+
return (newValue) => {
|
|
13
|
+
const value = !!newValue?.valueOf() && newValue !== 'false';
|
|
14
|
+
if (oldValue !== value) {
|
|
15
|
+
node.toggleAttribute(name, (oldValue = !!value));
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Element} node
|
|
22
|
+
* @param {String} name
|
|
23
|
+
* @return {(function(*): void)|*}
|
|
24
|
+
*/
|
|
25
|
+
const processPropertyAttribute = (node, name) => {
|
|
26
|
+
// TODO: It would be better if the .property= attribute would not be there in the first place...
|
|
27
|
+
node.removeAttribute(`.${name}`);
|
|
28
|
+
return (value) => {
|
|
29
|
+
node[name] = value;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Element} node
|
|
35
|
+
* @param {String} name
|
|
36
|
+
* @return {(function(*): void)|*}
|
|
37
|
+
*/
|
|
38
|
+
const processEventAttribute = (node, name) => {
|
|
39
|
+
// TODO: It would be better if the event attribute would not be there in the first place...
|
|
40
|
+
node.removeAttribute(name);
|
|
41
|
+
|
|
42
|
+
let oldValue = undefined;
|
|
43
|
+
let type = name.startsWith('@') ? name.slice(1) : name.toLowerCase().slice(2);
|
|
44
|
+
|
|
45
|
+
return (newValue) => {
|
|
46
|
+
if (oldValue !== newValue) {
|
|
47
|
+
if (oldValue) node.removeEventListener(type, oldValue);
|
|
48
|
+
if ((oldValue = newValue)) node.addEventListener(type, oldValue);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {Element} node
|
|
55
|
+
* @param {String} name
|
|
56
|
+
* @return {(function(*): void)|*}
|
|
57
|
+
*/
|
|
58
|
+
const processAttribute = (node, name) => {
|
|
59
|
+
let oldValue,
|
|
60
|
+
orphan = true;
|
|
61
|
+
const attributeNode = globalThis.document?.createAttributeNS(null, name);
|
|
62
|
+
return (newValue) => {
|
|
63
|
+
const value = newValue?.valueOf();
|
|
64
|
+
if (oldValue !== value) {
|
|
65
|
+
if ((oldValue = value) == null) {
|
|
66
|
+
if (!orphan) {
|
|
67
|
+
node.removeAttributeNode(attributeNode);
|
|
68
|
+
orphan = true;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
attributeNode.value = value;
|
|
72
|
+
if (orphan) {
|
|
73
|
+
node.setAttributeNodeNS(attributeNode);
|
|
74
|
+
orphan = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** @type {Map<Element, AttributePart>} */
|
|
82
|
+
const attributePartsCache = new WeakMap();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {Element} node
|
|
86
|
+
* @param {String} name
|
|
87
|
+
* @return {(function(*): void)|*}
|
|
88
|
+
*/
|
|
89
|
+
export const processAttributePart = (node, name) => {
|
|
90
|
+
// boolean attribute: ?boolean=${...}
|
|
91
|
+
if (name.startsWith('?')) {
|
|
92
|
+
return processBooleanAttribute(node, name.slice(1), false);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// property attribute: .property=${...}
|
|
96
|
+
if (name.startsWith('.')) {
|
|
97
|
+
return processPropertyAttribute(node, name.slice(1));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// event attribute: @event=${...} || "old school" event attribute: onevent=${...}
|
|
101
|
+
if (name.startsWith('@') || name.startsWith('on')) {
|
|
102
|
+
return processEventAttribute(node, name);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// normal "string" attribute: attribute=${...}
|
|
106
|
+
return processAttribute(node, name);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* For updating a single attribute
|
|
111
|
+
*/
|
|
112
|
+
export class AttributePart extends Part {
|
|
113
|
+
name = undefined;
|
|
114
|
+
interpolations = 1;
|
|
115
|
+
values = [];
|
|
116
|
+
currentValueIndex = 0;
|
|
117
|
+
initialValue = undefined;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {Comment} node
|
|
121
|
+
* @param {String} name
|
|
122
|
+
* @param {String} initialValue
|
|
123
|
+
*/
|
|
124
|
+
constructor(node, name, initialValue) {
|
|
125
|
+
// If we have multiple attribute parts with the same name, it means we have multiple
|
|
126
|
+
// interpolations inside that attribute. Instead of creating a new part, we will return the same
|
|
127
|
+
// as before and let it defer the update until the last interpolation gets updated
|
|
128
|
+
const attributePart = attributePartsCache.get(node.nextElementSibling);
|
|
129
|
+
if (attributePart && attributePart.name === name) {
|
|
130
|
+
attributePart.interpolations++;
|
|
131
|
+
node.__part = attributePart; // add Part to comment node for debugging in the browser
|
|
132
|
+
return attributePart;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
super();
|
|
136
|
+
this.name = name;
|
|
137
|
+
this.initialValue = initialValue;
|
|
138
|
+
this.processor = processAttributePart(node.nextElementSibling, this.name);
|
|
139
|
+
node.__part = this; // add Part to comment node for debugging in the browser
|
|
140
|
+
attributePartsCache.set(node.nextElementSibling, this);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string | number | bigint | boolean | undefined | symbol | null} value
|
|
145
|
+
*/
|
|
146
|
+
update(value) {
|
|
147
|
+
// If we only have one sole interpolation, we can just apply the update
|
|
148
|
+
if (this.initialValue === '\x03') {
|
|
149
|
+
this.processor(value);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Instead of applying the update immediately, we check if the part has multiple interpolations
|
|
154
|
+
// and store the values for each interpolation in a list
|
|
155
|
+
this.values[this.currentValueIndex++] = value;
|
|
156
|
+
|
|
157
|
+
// Only the last call to update (for the last interpolation) will actually trigger the update
|
|
158
|
+
// on the DOM processor. Here we can reset everything before the next round of updates
|
|
159
|
+
if (this.interpolations === this.currentValueIndex) {
|
|
160
|
+
this.currentValueIndex = 0;
|
|
161
|
+
let replaceIndex = 0;
|
|
162
|
+
// Note: this will coarse the values into strings, but it's probably ok since there can only be multiple values in string attributes?!
|
|
163
|
+
const parsedValue = this.initialValue.replace(/\x03/g, () => this.values[replaceIndex++]);
|
|
164
|
+
this.processor(parsedValue);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { COMMENT_NODE, ELEMENT_NODE } from '../util/DOMHelper.js';
|
|
2
|
+
import { Part } from './Part.js';
|
|
3
|
+
import { TemplateResult } from './TemplateResult.js';
|
|
4
|
+
import { TemplatePart } from './TemplatePart.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Node | TemplatePart | any[]} node
|
|
8
|
+
* @return {ChildNode}
|
|
9
|
+
*/
|
|
10
|
+
const removeChildNodes = (node) => {
|
|
11
|
+
const range = globalThis.document?.createRange();
|
|
12
|
+
const firstChild = Array.isArray(node)
|
|
13
|
+
? node[0]
|
|
14
|
+
: node instanceof TemplatePart
|
|
15
|
+
? node.childNodes[0]
|
|
16
|
+
: node.firstChild;
|
|
17
|
+
const lastChild = Array.isArray(node)
|
|
18
|
+
? node[node.length - 1]
|
|
19
|
+
: node instanceof TemplatePart
|
|
20
|
+
? node.childNodes[node.childNodes.length - 1]
|
|
21
|
+
: node.lastChild;
|
|
22
|
+
range.setStartAfter(firstChild);
|
|
23
|
+
range.setEndAfter(lastChild);
|
|
24
|
+
range.deleteContents();
|
|
25
|
+
return firstChild;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Element} commentNode
|
|
30
|
+
*/
|
|
31
|
+
const removeNodesBetweenComments = (commentNode) => {
|
|
32
|
+
while (commentNode.previousSibling) {
|
|
33
|
+
if (
|
|
34
|
+
commentNode.previousSibling.nodeType === COMMENT_NODE &&
|
|
35
|
+
commentNode.previousSibling.data === commentNode.data.replace('/', '')
|
|
36
|
+
) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
commentNode.parentNode.removeChild(commentNode.previousSibling);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Node} parentNode
|
|
45
|
+
* @param {Node[]} domChildNodes
|
|
46
|
+
* @param {Node[]} templateChildNodes
|
|
47
|
+
* @param {Node} anchorNode
|
|
48
|
+
* @return {Node[]}
|
|
49
|
+
*/
|
|
50
|
+
const diffChildNodes = function (parentNode, domChildNodes, templateChildNodes, anchorNode) {
|
|
51
|
+
// Diff each node in the child node lists
|
|
52
|
+
let length = Math.max(templateChildNodes.length, domChildNodes.length);
|
|
53
|
+
for (let index = 0; index < length; index++) {
|
|
54
|
+
let domChildNode = domChildNodes[index];
|
|
55
|
+
let templateChildNode = templateChildNodes[index];
|
|
56
|
+
|
|
57
|
+
// If the DOM node doesn't exist, append/copy the template node
|
|
58
|
+
if (!domChildNode) {
|
|
59
|
+
if (templateChildNode instanceof TemplatePart) {
|
|
60
|
+
anchorNode.before(...templateChildNode.childNodes);
|
|
61
|
+
} else if (typeof templateChildNode === 'object' && 'ELEMENT_NODE' in templateChildNode) {
|
|
62
|
+
parentNode.insertBefore(templateChildNode, anchorNode);
|
|
63
|
+
} else {
|
|
64
|
+
// TODO: this might not be performant?! Can we maybe handle this in processDomNode?!
|
|
65
|
+
// TODO: I think that we need to make ChildNodeParts from all primitive values as well
|
|
66
|
+
parentNode.insertBefore(document.createTextNode(templateChildNode), anchorNode);
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If the template node doesn't exist, remove the node in the DOM
|
|
72
|
+
if (!templateChildNode) {
|
|
73
|
+
parentNode.removeChild(removeChildNodes(domChildNode));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If DOM node is equal to the template node, don't do anything
|
|
78
|
+
if (domChildNode === templateChildNode) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Everything else is somehow different and can be replaced
|
|
83
|
+
parentNode.replaceChild(templateChildNode, domChildNode);
|
|
84
|
+
}
|
|
85
|
+
return templateChildNodes;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {Node} comment
|
|
90
|
+
* @param {TemplatePart | any[] | any} initialValue
|
|
91
|
+
* @return {(function(*): void)|*}
|
|
92
|
+
*/
|
|
93
|
+
export const processNodePart = (comment, initialValue) => {
|
|
94
|
+
let nodes = [];
|
|
95
|
+
let oldValue = initialValue instanceof TemplatePart ? [...initialValue.childNodes] : initialValue;
|
|
96
|
+
// 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.
|
|
97
|
+
let cachedTextNode = undefined;
|
|
98
|
+
|
|
99
|
+
const processNodeValue = (newValue) => {
|
|
100
|
+
switch (typeof newValue) {
|
|
101
|
+
// primitives are handled as text content
|
|
102
|
+
case 'string':
|
|
103
|
+
case 'number':
|
|
104
|
+
case 'boolean':
|
|
105
|
+
if (oldValue !== newValue) {
|
|
106
|
+
oldValue = newValue;
|
|
107
|
+
if (!cachedTextNode) cachedTextNode = globalThis.document?.createTextNode('');
|
|
108
|
+
cachedTextNode.data = newValue;
|
|
109
|
+
|
|
110
|
+
if (comment.previousSibling?.data === comment.data.replace('/', '')) {
|
|
111
|
+
// the part is empty - we haven't rendered it yet
|
|
112
|
+
comment.parentNode.insertBefore(cachedTextNode, comment);
|
|
113
|
+
} else {
|
|
114
|
+
// the part was already rendered (either server side or on the client)
|
|
115
|
+
comment.previousSibling.data = newValue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
nodes = [cachedTextNode];
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
// null (= typeof "object") and undefined are used to clean up previous content
|
|
122
|
+
case 'object':
|
|
123
|
+
case 'undefined':
|
|
124
|
+
if (newValue === null || newValue === undefined) {
|
|
125
|
+
if (oldValue !== newValue) {
|
|
126
|
+
oldValue = newValue;
|
|
127
|
+
removeNodesBetweenComments(comment);
|
|
128
|
+
nodes = [];
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(newValue)) {
|
|
133
|
+
if (newValue.length === 0) {
|
|
134
|
+
removeNodesBetweenComments(comment);
|
|
135
|
+
nodes = [];
|
|
136
|
+
} else {
|
|
137
|
+
nodes = diffChildNodes(comment.parentNode, oldValue || [], newValue, comment);
|
|
138
|
+
}
|
|
139
|
+
oldValue = newValue;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
// if the new value is a node or a fragment, and it's different from the live node, then it's diffed.
|
|
143
|
+
if (oldValue !== newValue) {
|
|
144
|
+
if ('ELEMENT_NODE' in newValue) {
|
|
145
|
+
oldValue = newValue;
|
|
146
|
+
nodes = diffChildNodes(comment.parentNode, nodes, [newValue], comment);
|
|
147
|
+
} else if (newValue instanceof TemplatePart) {
|
|
148
|
+
oldValue = newValue;
|
|
149
|
+
nodes = diffChildNodes(comment.parentNode, nodes, [...newValue.childNodes], comment);
|
|
150
|
+
} else {
|
|
151
|
+
console.warn('Could not process value', newValue);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case 'function':
|
|
156
|
+
processNodeValue(newValue(comment));
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return processNodeValue;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* For updating a text node, a sequence of child nodes or an array of `any` values
|
|
166
|
+
*/
|
|
167
|
+
export class ChildNodePart extends Part {
|
|
168
|
+
/** @type {Node | undefined} */
|
|
169
|
+
startNode = undefined;
|
|
170
|
+
|
|
171
|
+
/** @type {Node | undefined} */
|
|
172
|
+
endNode = undefined;
|
|
173
|
+
|
|
174
|
+
// TODO: this is only for array values
|
|
175
|
+
/** @type {Part[]} */
|
|
176
|
+
parts = [];
|
|
177
|
+
|
|
178
|
+
// TODO: this is only for TemplatePart values
|
|
179
|
+
templatePart = undefined;
|
|
180
|
+
|
|
181
|
+
// TODO: maybe we can store a generic vale|parsedValue that can be either parts[] | templatePart | any
|
|
182
|
+
|
|
183
|
+
/** @type {Node[]} */
|
|
184
|
+
childNodes = [];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {Node} startNode - the start comment marker node
|
|
188
|
+
* @param {any | any[]} value
|
|
189
|
+
*/
|
|
190
|
+
constructor(startNode, value) {
|
|
191
|
+
if (startNode && startNode?.nodeType !== COMMENT_NODE) {
|
|
192
|
+
throw new Error('ChildNodePart: startNode is not a comment node');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
super();
|
|
196
|
+
|
|
197
|
+
let serverSideRendered = false;
|
|
198
|
+
if (startNode) {
|
|
199
|
+
startNode.__part = this; // add Part to comment node for debugging in the browser
|
|
200
|
+
|
|
201
|
+
const placeholder = startNode.data;
|
|
202
|
+
const childNodes = [startNode];
|
|
203
|
+
let childNode = startNode.nextSibling;
|
|
204
|
+
while (childNode && childNode.data !== `/${placeholder}`) {
|
|
205
|
+
childNodes.push(childNode);
|
|
206
|
+
childNode = childNode.nextSibling;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const endNode = childNode;
|
|
210
|
+
childNodes.push(endNode);
|
|
211
|
+
|
|
212
|
+
// if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
|
|
213
|
+
if (childNodes.length > 2) {
|
|
214
|
+
serverSideRendered = true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.childNodes = childNodes;
|
|
218
|
+
this.startNode = startNode;
|
|
219
|
+
this.endNode = endNode;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// value can become array | TemplatePart | any
|
|
223
|
+
const initialValue = this.parseValue(value);
|
|
224
|
+
|
|
225
|
+
if (this.endNode) {
|
|
226
|
+
this.processor = processNodePart(this.endNode, serverSideRendered ? initialValue : undefined);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!serverSideRendered) {
|
|
230
|
+
if (value instanceof TemplateResult || Array.isArray(value)) {
|
|
231
|
+
this.updateParts(value);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 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.
|
|
235
|
+
this.childNodes = [...this.childNodes];
|
|
236
|
+
this.processor?.(initialValue);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {any} value
|
|
242
|
+
* @return {*}
|
|
243
|
+
*/
|
|
244
|
+
update(value) {
|
|
245
|
+
const parsedValue = this.parseValue(value);
|
|
246
|
+
|
|
247
|
+
if (value instanceof TemplateResult || Array.isArray(value)) {
|
|
248
|
+
this.updateParts(value);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.processor?.(parsedValue);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {TemplateResult | any[]} value
|
|
256
|
+
*/
|
|
257
|
+
updateParts(value) {
|
|
258
|
+
if (value instanceof TemplateResult) {
|
|
259
|
+
this.templatePart.update(value);
|
|
260
|
+
}
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
for (let index = 0; index < value.length; index++) {
|
|
263
|
+
this.parts[index]?.update(value[index]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {TemplateResult | any[] | any} value
|
|
270
|
+
* @return {TemplatePart | any[] | any}
|
|
271
|
+
*/
|
|
272
|
+
parseValue(value) {
|
|
273
|
+
if (value instanceof TemplateResult) {
|
|
274
|
+
if (!this.templatePart) {
|
|
275
|
+
const templatePartCommentNodes = this.childNodes?.filter(
|
|
276
|
+
(node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
|
|
277
|
+
);
|
|
278
|
+
const startNode = templatePartCommentNodes[0];
|
|
279
|
+
this.templatePart = new TemplatePart(startNode, value);
|
|
280
|
+
}
|
|
281
|
+
return this.templatePart;
|
|
282
|
+
}
|
|
283
|
+
if (Array.isArray(value)) {
|
|
284
|
+
return this.parseArray(value);
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Nested TemplateResults values need to be unrolled in order for update functions to be able to process them
|
|
291
|
+
* @param {any[]} values
|
|
292
|
+
* @return {any[]}
|
|
293
|
+
*/
|
|
294
|
+
parseArray(values) {
|
|
295
|
+
const parsedValues = [];
|
|
296
|
+
for (let index = 0; index < values.length; index++) {
|
|
297
|
+
let value = values[index];
|
|
298
|
+
|
|
299
|
+
if (value instanceof TemplateResult) {
|
|
300
|
+
let templatePart = this.parts[index];
|
|
301
|
+
if (!templatePart) {
|
|
302
|
+
const templatePartCommentNodes = this.childNodes?.filter(
|
|
303
|
+
(node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
|
|
304
|
+
);
|
|
305
|
+
const startNode = templatePartCommentNodes[index];
|
|
306
|
+
templatePart = new TemplatePart(startNode, value);
|
|
307
|
+
this.parts[index] = templatePart;
|
|
308
|
+
}
|
|
309
|
+
parsedValues[index] = templatePart;
|
|
310
|
+
} else if (Array.isArray(value)) {
|
|
311
|
+
let childNodePart = this.parts[index];
|
|
312
|
+
if (!childNodePart) {
|
|
313
|
+
// TODO: this seems not correct :(
|
|
314
|
+
const templatePartCommentNodes = this.childNodes?.filter(
|
|
315
|
+
(node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
|
|
316
|
+
);
|
|
317
|
+
const startNode = templatePartCommentNodes[index];
|
|
318
|
+
childNodePart = new ChildNodePart(startNode, value);
|
|
319
|
+
this.parts[index] = childNodePart;
|
|
320
|
+
}
|
|
321
|
+
parsedValues[index] = childNodePart;
|
|
322
|
+
} else {
|
|
323
|
+
parsedValues[index] = typeof value === 'function' ? value() : value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return parsedValues;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Part } from './Part.js';
|
|
2
|
+
import { isObjectLike } from '../util/AttributeParser.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* For updating a directive on a node
|
|
6
|
+
*/
|
|
7
|
+
export class NodePart extends Part {
|
|
8
|
+
/** @type {Node} */
|
|
9
|
+
node = undefined;
|
|
10
|
+
|
|
11
|
+
/** @type {Directive} */
|
|
12
|
+
directive = undefined;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Node} node
|
|
16
|
+
* @param {() => {}} initialValue
|
|
17
|
+
*/
|
|
18
|
+
constructor(node, initialValue) {
|
|
19
|
+
if (!isObjectLike(initialValue) && !initialValue.directiveClass) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'NodePart: value is not a wrapped directive function. You must wrap you custom directive class with the defineDirective function.',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
super();
|
|
25
|
+
node.__part = this; // add Part to comment node for debugging in the browser
|
|
26
|
+
this.node = node.nextElementSibling;
|
|
27
|
+
const { directiveClass, values } = initialValue;
|
|
28
|
+
this.directive = new directiveClass(node.nextElementSibling);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {() => {}} value
|
|
33
|
+
*/
|
|
34
|
+
update(value) {
|
|
35
|
+
const { directiveClass, values } = value;
|
|
36
|
+
this.directive.update(...values);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { COMMENT_NODE, convertStringToTemplate } from '../util/DOMHelper.js';
|
|
2
|
+
import { Part } from './Part.js';
|
|
3
|
+
import { TemplateResult } from './TemplateResult.js';
|
|
4
|
+
import { AttributePart } from './AttributePart.js';
|
|
5
|
+
import { ChildNodePart } from './ChildNodePart.js';
|
|
6
|
+
import { NodePart } from './NodePart.js';
|
|
7
|
+
|
|
8
|
+
/** @type {Map<TemplateStringsArray, Part[]>} */
|
|
9
|
+
const partsCache = new WeakMap();
|
|
10
|
+
|
|
11
|
+
/** @type {Map<TemplateStringsArray, DocumentFragment>} */
|
|
12
|
+
const fragmentsCache = new WeakMap();
|
|
13
|
+
|
|
14
|
+
export class TemplatePart extends Part {
|
|
15
|
+
/** @type {Part[]} */
|
|
16
|
+
parts = [];
|
|
17
|
+
|
|
18
|
+
/** @type {TemplateStringsArray} */
|
|
19
|
+
strings = undefined;
|
|
20
|
+
|
|
21
|
+
/** @type {Node[]} */
|
|
22
|
+
childNodes = [];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Node} startNode - the start comment marker node
|
|
26
|
+
* @param {TemplateResult} value
|
|
27
|
+
*/
|
|
28
|
+
constructor(startNode, value) {
|
|
29
|
+
if (startNode && startNode?.nodeType !== COMMENT_NODE) {
|
|
30
|
+
throw new Error('TemplatePart: startNode is not a comment node');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
super();
|
|
34
|
+
|
|
35
|
+
let serverSideRendered = false;
|
|
36
|
+
if (startNode) {
|
|
37
|
+
startNode.__part = this; // add Part to comment node for debugging in the browser
|
|
38
|
+
|
|
39
|
+
const placeholder = startNode.data;
|
|
40
|
+
const childNodes = [startNode];
|
|
41
|
+
let childNode = startNode.nextSibling;
|
|
42
|
+
while (childNode && childNode.data !== `/${placeholder}`) {
|
|
43
|
+
childNodes.push(childNode);
|
|
44
|
+
childNode = childNode.nextSibling;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const endNode = childNode;
|
|
48
|
+
childNodes.push(endNode);
|
|
49
|
+
|
|
50
|
+
// if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
|
|
51
|
+
if (childNodes.length > 2) {
|
|
52
|
+
serverSideRendered = true;
|
|
53
|
+
this.childNodes = childNodes;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.parseValue(value);
|
|
58
|
+
|
|
59
|
+
if (!serverSideRendered) {
|
|
60
|
+
this.updateParts(value.values);
|
|
61
|
+
// 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.
|
|
62
|
+
this.childNodes = [...this.childNodes];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {TemplateResult} value
|
|
68
|
+
*/
|
|
69
|
+
update(value) {
|
|
70
|
+
this.parseValue(value);
|
|
71
|
+
this.updateParts(value.values);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {any[]} values
|
|
76
|
+
*/
|
|
77
|
+
updateParts(values) {
|
|
78
|
+
for (let index = 0; index < values.length; index++) {
|
|
79
|
+
this.parts[index]?.update(values[index]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {TemplateResult} templateResult
|
|
85
|
+
*/
|
|
86
|
+
parseValue(templateResult) {
|
|
87
|
+
if (this.strings !== templateResult.strings) {
|
|
88
|
+
if (this.childNodes.length === 0) {
|
|
89
|
+
let fragment = fragmentsCache.get(templateResult.strings);
|
|
90
|
+
if (!fragment) {
|
|
91
|
+
fragment = this.parseFragment(templateResult);
|
|
92
|
+
fragmentsCache.set(templateResult.strings, fragment);
|
|
93
|
+
}
|
|
94
|
+
const importedFragment = globalThis.document?.importNode(fragment, true);
|
|
95
|
+
this.childNodes = importedFragment.childNodes;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let parts = partsCache.get(templateResult.strings);
|
|
99
|
+
if (!parts) {
|
|
100
|
+
parts = templateResult.parseParts(this.childNodes);
|
|
101
|
+
partsCache.set(templateResult.strings, parts);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.parts = parts
|
|
105
|
+
.map((part) => {
|
|
106
|
+
// We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
|
|
107
|
+
part.node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
|
|
108
|
+
return part;
|
|
109
|
+
})
|
|
110
|
+
.map((part, index) => {
|
|
111
|
+
if (part.type === 'node') {
|
|
112
|
+
return new ChildNodePart(part.node, templateResult.values[index]);
|
|
113
|
+
}
|
|
114
|
+
if (part.type === 'attribute') {
|
|
115
|
+
return new AttributePart(part.node, part.name, 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
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {TemplateResult} templateResult
|
|
128
|
+
* @return {DocumentFragment}
|
|
129
|
+
*/
|
|
130
|
+
parseFragment(templateResult) {
|
|
131
|
+
const templateString = templateResult.templateString;
|
|
132
|
+
return convertStringToTemplate(templateString);
|
|
133
|
+
}
|
|
134
|
+
}
|