@webtides/element-js 1.0.0-alpha.9 → 1.1.0

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +369 -0
  2. package/README.md +3 -3
  3. package/docs/README.md +70 -89
  4. package/docs/concepts/classes.md +37 -66
  5. package/docs/concepts/hierarchy.md +29 -31
  6. package/docs/concepts/lifecycle.md +9 -23
  7. package/docs/concepts/reactivity.md +1 -3
  8. package/docs/features/computed-properties.md +13 -16
  9. package/docs/features/dependency-injection-context-protocol.md +75 -87
  10. package/docs/features/global-styles.md +1 -1
  11. package/docs/features/methods.md +14 -15
  12. package/docs/features/properties.md +21 -33
  13. package/docs/features/refs.md +23 -31
  14. package/docs/features/renderless.md +31 -37
  15. package/docs/features/store.md +63 -78
  16. package/docs/features/templates.md +4 -1
  17. package/docs/features/watch.md +28 -35
  18. package/docs/getting-started/upgrade-to-v1.md +16 -0
  19. package/docs/guide/guide.md +10 -22
  20. package/index.js +29 -2
  21. package/package.json +62 -62
  22. package/src/BaseElement.js +637 -632
  23. package/src/StyledElement.js +104 -104
  24. package/src/TemplateElement.js +53 -58
  25. package/src/dom-parts/AttributePart.js +116 -116
  26. package/src/dom-parts/ChildNodePart.js +288 -288
  27. package/src/dom-parts/NodePart.js +27 -27
  28. package/src/dom-parts/Part.js +6 -6
  29. package/src/dom-parts/TemplatePart.js +119 -119
  30. package/src/dom-parts/TemplateResult.js +274 -267
  31. package/src/dom-parts/directives.js +105 -48
  32. package/src/dom-parts/html.js +2 -2
  33. package/src/dom-parts/render.js +6 -1
  34. package/src/util/AttributeParser.js +47 -47
  35. package/src/util/DOMHelper.js +190 -191
  36. package/src/util/Directive.js +31 -24
  37. package/src/util/GlobalStylesStore.js +56 -49
  38. package/src/util/SerializeStateHelper.js +56 -56
  39. package/src/util/Store.js +174 -174
  40. package/src/util/defineElement.js +5 -5
  41. package/src/util/toString.js +5 -5
@@ -1,4 +1,4 @@
1
- import { COMMENT_NODE, getNodePath } from '../util/DOMHelper';
1
+ import { COMMENT_NODE, getNodePath } from '../util/DOMHelper.js';
2
2
  import { encodeAttribute, isObjectLike } from '../util/AttributeParser.js';
3
3
  import { TemplatePart } from './TemplatePart.js';
4
4
 
@@ -22,52 +22,50 @@ const interpolation = new RegExp(`(<!--dom-part-(\\d+)--><!--/dom-part-(\\d+)-->
22
22
  * @returns {string} X/HTML with prefixed comments or attributes
23
23
  */
24
24
  export const createTemplateString = (templateStrings, attributePlaceholders = '') => {
25
- // TODO: make it easier to identify attribute and node parts for SSR and leave the comments at those positions to be replaced in toString()
26
- let partIndex = 0;
27
- // join all interpolations (for values) with a special placeholder and remove any whitespace
28
- let template = templateStrings.join('\x01').trim();
29
- // find (match) all elements to identify their attributes
30
- template = template.replace(elements, (_, name, attributesString, trailingSlash) => {
31
- let elementTagWithAttributes = name + attributesString.replaceAll('\x01', attributePlaceholders).trimEnd();
32
- if (trailingSlash.length) elementTagWithAttributes += voidElements.test(name) ? ' /' : '></' + name;
33
- // collect all attribute parts so that we can place matching comment nodes
34
- const attributeParts = attributesString.replace(attributes, (attribute, name, valueWithQuotes, directive) => {
35
- if (directive && directive === '\x01') {
36
- return `<!--\x02$-->`;
37
- }
25
+ // TODO: make it easier to identify attribute and node parts for SSR and leave the comments at those positions to be replaced in toString()
26
+ let partIndex = 0;
27
+ // join all interpolations (for values) with a special placeholder and remove any whitespace
28
+ let template = templateStrings.join('\x01').trim();
29
+ // find (match) all elements to identify their attributes
30
+ template = template.replace(elements, (_, name, attributesString, trailingSlash) => {
31
+ let elementTagWithAttributes = name + attributesString.replaceAll('\x01', attributePlaceholders).trimEnd();
32
+ // TODO the closing case is weird.
33
+ if (trailingSlash.length) elementTagWithAttributes += voidElements.test(name) ? ' /' : '></' + name;
34
+ // collect all attribute parts so that we can place matching comment nodes
35
+ const attributeParts = attributesString.replace(attributes, (attribute, name, valueWithQuotes, directive) => {
36
+ if (directive && directive === '\x01') {
37
+ return `<!--\x02$-->`;
38
+ }
38
39
 
39
- // remove quotes from attribute value to normalize the value
40
- const value =
41
- valueWithQuotes?.startsWith('"') || valueWithQuotes?.startsWith("'")
42
- ? valueWithQuotes.slice(1, -1)
43
- : valueWithQuotes;
44
- const partsCount = (attribute.match(/\x01/g) || []).length;
45
- const parts = [];
46
- for (let index = 0; index < partsCount; index++) {
47
- parts.push(`<!--\x02:${name}=${value.replaceAll('\x01', '\x03')}-->`);
48
- }
49
- return parts.join('');
50
- });
51
- return `
52
- ${attributesString.includes('\x01') ? attributeParts : ''}
53
- <${elementTagWithAttributes}>
54
- `;
55
- });
56
- // replace interpolation placeholders with our indexed markers
57
- template = template.replace(partPositions, (partPosition) => {
58
- if (partPosition === '\x01') {
59
- return `<!--dom-part-${partIndex}--><!--/dom-part-${partIndex++}-->`;
60
- }
61
- if (partPosition === '\x02') {
62
- return `dom-part-${partIndex++}`;
63
- }
64
- });
65
- return `
66
- <!--template-part-->
67
- ${template}
68
- <!--/template-part-->
69
- `;
70
- // TODO: ^ this is actually important because of the whitespace
40
+ // remove quotes from attribute value to normalize the value
41
+ const value =
42
+ valueWithQuotes?.startsWith('"') || valueWithQuotes?.startsWith("'")
43
+ ? valueWithQuotes.slice(1, -1)
44
+ : valueWithQuotes;
45
+ const partsCount = (attribute.match(/\x01/g) || []).length;
46
+ const parts = [];
47
+ for (let index = 0; index < partsCount; index++) {
48
+ parts.push(`<!--\x02:${name}=${value.replaceAll('\x01', '\x03')}-->`);
49
+ }
50
+ return parts.join('');
51
+ });
52
+ // TODO create test to check if lf are in place
53
+ return `
54
+ ${attributesString.includes('\x01') ? attributeParts : ''}\n
55
+ <${elementTagWithAttributes}>\n
56
+ `.trim();
57
+ });
58
+ // replace interpolation placeholders with our indexed markers
59
+ template = template.replace(partPositions, (partPosition) => {
60
+ if (partPosition === '\x01') {
61
+ return `<!--dom-part-${partIndex}--><!--/dom-part-${partIndex++}-->`;
62
+ } else if (partPosition === '\x02') {
63
+ return `dom-part-${partIndex++}`;
64
+ }
65
+ });
66
+ // TODO create test to check if lf are in place
67
+ // /n is important in the returns as we expect a certain order of text / comments an nodes
68
+ return `<!--template-part-->\n${template}\n<!--/template-part-->`.trim();
71
69
  };
72
70
 
73
71
  /**
@@ -77,9 +75,9 @@ export const createTemplateString = (templateStrings, attributePlaceholders = ''
77
75
  * @return {String}
78
76
  */
79
77
  const attribute = (name, value, isSingleValue = true) => {
80
- return ` ${name}="${encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value)}${
81
- isSingleValue ? '"' : ''
82
- }`;
78
+ return ` ${name}="${encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value)}${
79
+ isSingleValue ? '"' : ''
80
+ }`;
83
81
  };
84
82
 
85
83
  /**
@@ -87,26 +85,30 @@ const attribute = (name, value, isSingleValue = true) => {
87
85
  * @return {String}
88
86
  */
89
87
  const getValue = (value) => {
90
- switch (typeof value) {
91
- case 'string':
92
- return encodeAttribute(value);
93
- case 'boolean':
94
- case 'number':
95
- return String(value);
96
- case 'object':
97
- switch (true) {
98
- case Array.isArray(value):
99
- return value.map(getValue).join('');
100
- case value instanceof TemplateResult:
101
- return value.toString();
102
- case value.__unsafeHTML:
103
- return value.string;
104
- }
105
- break;
106
- case 'function':
107
- return getValue(value());
108
- }
109
- return value == null ? '' : encodeAttribute(String(value));
88
+ switch (typeof value) {
89
+ case 'string':
90
+ return encodeAttribute(value);
91
+ case 'boolean':
92
+ case 'number':
93
+ return String(value);
94
+ case 'object':
95
+ switch (true) {
96
+ case value === null:
97
+ return '';
98
+ case Array.isArray(value):
99
+ return value.map(getValue).join('');
100
+ case value instanceof TemplateResult:
101
+ return value.toString();
102
+ case value.__unsafeHTML:
103
+ return value.string;
104
+ default:
105
+ console.log('Cannot getValue for', value);
106
+ }
107
+ break;
108
+ case 'function':
109
+ return getValue(value());
110
+ }
111
+ return value == null ? '' : encodeAttribute(String(value));
110
112
  };
111
113
 
112
114
  /** @type {Map<TemplateStringsArray, *[]>} */
@@ -116,218 +118,223 @@ const parsedUpdates = new WeakMap();
116
118
  const templateParts = new WeakMap();
117
119
 
118
120
  export class TemplateResult {
119
- /**
120
- * @param {TemplateStringsArray} strings
121
- * @param {any[]} values
122
- */
123
- constructor(strings, ...values) {
124
- this.strings = strings;
125
- this.values = values;
126
- }
121
+ /**
122
+ * @param {TemplateStringsArray} strings
123
+ * @param {any[]} values
124
+ */
125
+ constructor(strings, ...values) {
126
+ this.strings = strings;
127
+ this.values = values;
128
+ }
127
129
 
128
- /**
129
- * @param {Element} domNode
130
- */
131
- renderInto(domNode) {
132
- let serverSideRendered = false;
133
- let templatePart = templateParts.get(domNode);
134
- if (!templatePart) {
135
- const startNode = Array.from(domNode.childNodes)
136
- .filter((node) => node.nodeType === COMMENT_NODE)
137
- .find((node) => node.data === 'template-part');
130
+ /**
131
+ * @param {Element} domNode
132
+ */
133
+ renderInto(domNode) {
134
+ let serverSideRendered = false;
135
+ let templatePart = templateParts.get(domNode);
136
+ if (!templatePart) {
137
+ const startNode = Array.from(domNode.childNodes)
138
+ .filter((node) => node.nodeType === COMMENT_NODE)
139
+ .find((node) => node.data === 'template-part');
138
140
 
139
- serverSideRendered = startNode !== undefined;
141
+ serverSideRendered = startNode !== undefined;
140
142
 
141
- templatePart = new TemplatePart(startNode, this);
142
- templateParts.set(domNode, templatePart);
143
+ templatePart = new TemplatePart(startNode, this);
144
+ templateParts.set(domNode, templatePart);
143
145
 
144
- if (!serverSideRendered) {
145
- domNode.replaceChildren(...templatePart.childNodes);
146
- }
147
- } else {
148
- templatePart.update(this);
149
- }
150
- }
146
+ if (!serverSideRendered) {
147
+ domNode.replaceChildren(...templatePart.childNodes);
148
+ }
149
+ } else {
150
+ templatePart.update(this);
151
+ }
152
+ }
151
153
 
152
- /**
153
- * @param {String[]} strings
154
- * @return {(() => String)[]}
155
- */
156
- parse(strings) {
157
- const html = createTemplateString(strings, '\x04');
158
- const parts = [];
159
- let i = 0;
160
- let match = null;
161
- while ((match = interpolation.exec(html))) {
162
- const pre = html.slice(i, match.index);
163
- i = match.index + match[0].length;
164
- if (match[2]) {
165
- // ChildNodePart
166
- const index = match[2];
167
- parts.push((value) => `${pre}<!--dom-part-${index}-->${getValue(value)}<!--/dom-part-${index}-->`);
168
- } else if (match[4]) {
169
- // AttributePart with single interpolation or the first interpolation right after the attribute=
170
- const isSingleValue = match[5] !== undefined;
171
- let name = match[4];
172
- switch (true) {
173
- case name[0] === '?':
174
- const booleanName = name.slice(1).toLowerCase();
175
- parts.push((value) => {
176
- if (!value) return '';
177
- return `${pre} ${booleanName}=""`;
178
- });
179
- break;
180
- case name[0] === '.':
181
- const lower = name.slice(1).toLowerCase();
182
- parts.push((value) => {
183
- let result = pre;
184
- // null, undefined, and false are not shown at all
185
- if (value === null || value === undefined || value === '') {
186
- result += attribute(lower, '');
187
- } else {
188
- // in all other cases, just escape it in quotes
189
- result += attribute(lower, value, isSingleValue);
190
- }
191
- return result;
192
- });
193
- break;
194
- case name[0] === '@':
195
- name = 'on' + name.slice(1);
196
- case name[0] === 'o' && name[1] === 'n':
197
- parts.push((value) => {
198
- return pre;
199
- });
200
- break;
201
- default:
202
- parts.push((value) => {
203
- let result = pre;
204
- if (value != null) {
205
- result += attribute(name, value, isSingleValue);
206
- }
207
- return result;
208
- });
209
- break;
210
- }
211
- } else {
212
- // AttributePart in the middle of an attribute value or NodePart
213
- parts.push((value) => {
214
- let result = pre;
215
- // TODO: we currently cannot distinguish between NodeParts and AttributeParts before
216
- if (isObjectLike(value) && value.directiveClass) {
217
- // NodePart
218
- const directive = new value.directiveClass();
219
- result += directive.stringify(...value.values);
220
- } else if (value != null) {
221
- result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
222
- }
223
- return result;
224
- });
225
- }
226
- }
154
+ /**
155
+ * @param {String[]} strings
156
+ * @return {(() => String)[]}
157
+ */
158
+ parse(strings) {
159
+ const html = createTemplateString(strings, '\x04');
160
+ const parts = [];
161
+ let i = 0;
162
+ let match = null;
163
+ while ((match = interpolation.exec(html))) {
164
+ const pre = html.slice(i, match.index);
165
+ i = match.index + match[0].length;
166
+ if (match[2]) {
167
+ // ChildNodePart
168
+ const index = match[2];
169
+ parts.push((value) => `${pre}<!--dom-part-${index}-->${getValue(value)}<!--/dom-part-${index}-->`);
170
+ } else if (match[4]) {
171
+ // AttributePart with single interpolation or the first interpolation right after the attribute=
172
+ const isSingleValue = match[5] !== undefined;
173
+ let name = match[4];
174
+ switch (true) {
175
+ case name[0] === '?':
176
+ const booleanName = name.slice(1).toLowerCase();
177
+ parts.push((value) => {
178
+ if (!value) return '';
179
+ return `${pre} ${booleanName}=""`;
180
+ });
181
+ break;
182
+ case name[0] === '.':
183
+ const lower = name.slice(1).toLowerCase();
184
+ parts.push((value) => {
185
+ let result = pre;
186
+ // null, undefined, and false are not shown at all
187
+ if (value === null || value === undefined || value === '') {
188
+ result += attribute(lower, '');
189
+ } else {
190
+ // in all other cases, just escape it in quotes
191
+ result += attribute(lower, value, isSingleValue);
192
+ }
193
+ return result;
194
+ });
195
+ break;
196
+ case name[0] === '@':
197
+ name = 'on' + name.slice(1);
198
+ case name[0] === 'o' && name[1] === 'n':
199
+ parts.push((value) => {
200
+ return pre;
201
+ });
202
+ break;
203
+ default:
204
+ parts.push((value) => {
205
+ let result = pre;
206
+ if (value != null) {
207
+ result += attribute(name, value, isSingleValue);
208
+ }
209
+ return result;
210
+ });
211
+ break;
212
+ }
213
+ } else {
214
+ // AttributePart in the middle of an attribute value or NodePart
215
+ parts.push((value) => {
216
+ let result = pre;
217
+ // TODO: we currently cannot distinguish between NodeParts and AttributeParts before
218
+ if (isObjectLike(value) && value.directiveClass) {
219
+ // NodePart
220
+ const directive = new value.directiveClass();
221
+ result += directive.stringify(...value.values);
222
+ } else if (value != null) {
223
+ result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
224
+ }
225
+ return result;
226
+ });
227
+ }
228
+ }
227
229
 
228
- // We couldn't correctly parse parts from the template
229
- if (parts.length !== strings.length - 1) {
230
- throw {
231
- name: 'ParseTemplateError',
232
- message: 'Could not parse parts from template correctly. Parts length has not the expected length.',
233
- strings,
234
- templateString: html,
235
- parts,
236
- };
237
- }
230
+ // We couldn't correctly parse parts from the template
231
+ if (parts.length !== strings.length - 1) {
232
+ throw {
233
+ name: 'ParseTemplateError',
234
+ message: 'Could not parse parts from template correctly. Parts length has not the expected length.',
235
+ strings,
236
+ templateString: html,
237
+ parts,
238
+ };
239
+ }
238
240
 
239
- if (parts.length) {
240
- const last = parts[parts.length - 1];
241
- const chunk = html.slice(i);
242
- parts[parts.length - 1] = (value) => last(value) + chunk;
243
- } else {
244
- parts.push(() => html);
245
- }
246
- return parts;
247
- }
241
+ if (parts.length) {
242
+ const last = parts[parts.length - 1];
243
+ const chunk = html.slice(i);
244
+ parts[parts.length - 1] = (value) => last(value) + chunk;
245
+ } else {
246
+ parts.push(() => html);
247
+ }
248
+ return parts;
249
+ }
248
250
 
249
- /**
250
- * @param {Node[]} childNodes
251
- * @return {object[]}
252
- */
253
- parseParts(childNodes) {
254
- // we always create a template fragment so that we can start at the root for traversing the node path
255
- const range = globalThis.document?.createRange();
256
- range.setStartBefore(childNodes[0]);
257
- range.setEndAfter(childNodes[childNodes.length - 1]);
258
- const template = range.cloneContents();
251
+ /**
252
+ * @param {Node[]} childNodes
253
+ * @return {object[]}
254
+ */
255
+ parseParts(childNodes) {
256
+ // we always create a template fragment so that we can start at the root for traversing the node path
257
+ const range = globalThis.document?.createRange();
258
+ range.setStartBefore(childNodes[0]);
259
+ range.setEndAfter(childNodes[childNodes.length - 1]);
260
+ const template = range.cloneContents();
259
261
 
260
- const treeWalker = globalThis.document?.createTreeWalker(template, 128);
261
- let node = treeWalker.nextNode(); //to skip the root template-part
262
- const parts = [];
262
+ const treeWalker = globalThis.document?.createTreeWalker(template, 128);
263
+ let node = treeWalker.nextNode(); //to skip the root template-part
264
+ const parts = [];
263
265
 
264
- let nestedLevel = 0;
265
- // search for parts through numbered comment nodes with placeholders
266
- while ((node = treeWalker.nextNode())) {
267
- if (/^template-part$/.test(node.data)) {
268
- nestedLevel++;
269
- continue;
270
- }
271
- if (/^\/template-part$/.test(node.data)) {
272
- if (nestedLevel > 0) {
273
- nestedLevel--;
274
- }
275
- continue;
276
- }
277
- if (nestedLevel > 0) {
278
- continue;
279
- }
280
- if (/^dom-part-\d+$/.test(node.data)) {
281
- parts.push({ type: 'node', path: getNodePath(node) });
282
- continue;
283
- }
284
- if (/^dom-part-\d+:/.test(node.data)) {
285
- const [_, ...attribute] = node.data.split(':');
286
- const [name, initialValue] = attribute.join(':').split('=');
287
- parts.push({ type: 'attribute', path: getNodePath(node), name: name, initialValue });
288
- continue;
289
- }
290
- if (/^dom-part-\d+\$/.test(node.data)) {
291
- parts.push({ type: 'directive', path: getNodePath(node) });
292
- }
293
- }
266
+ let nestedLevel = 0;
267
+ // search for parts through numbered comment nodes with placeholders
268
+ while ((node = treeWalker.nextNode())) {
269
+ if (/^template-part$/.test(node.data)) {
270
+ nestedLevel++;
271
+ continue;
272
+ }
273
+ if (/^\/template-part$/.test(node.data)) {
274
+ if (nestedLevel > 0) {
275
+ nestedLevel--;
276
+ }
277
+ continue;
278
+ }
279
+ if (nestedLevel > 0) {
280
+ continue;
281
+ }
282
+ if (/^dom-part-\d+$/.test(node.data)) {
283
+ parts.push({ type: 'node', path: getNodePath(node) });
284
+ continue;
285
+ }
286
+ if (/^dom-part-\d+:/.test(node.data)) {
287
+ const [_, ...attribute] = node.data.split(':');
288
+ const [name, ...initialValue] = attribute.join(':').split('=');
289
+ parts.push({
290
+ type: 'attribute',
291
+ path: getNodePath(node),
292
+ name: name,
293
+ initialValue: initialValue.join('='),
294
+ });
295
+ continue;
296
+ }
297
+ if (/^dom-part-\d+\$/.test(node.data)) {
298
+ parts.push({ type: 'directive', path: getNodePath(node) });
299
+ }
300
+ }
294
301
 
295
- // We couldn't correctly parse parts from the template
296
- if (parts.length !== this.strings.length - 1) {
297
- throw {
298
- name: 'ParseTemplateError',
299
- message:
300
- 'Could not parse parts from template correctly. Parts length has not the expected length. -> ' +
301
- JSON.stringify({
302
- strings: this.strings,
303
- templateString: this.templateString,
304
- childNodes,
305
- template,
306
- parts,
307
- }),
308
- };
309
- }
302
+ // We couldn't correctly parse parts from the template
303
+ if (parts.length !== this.strings.length - 1) {
304
+ throw {
305
+ name: 'ParseTemplateError',
306
+ message:
307
+ 'Could not parse parts from template correctly. Parts length has not the expected length. -> ' +
308
+ JSON.stringify({
309
+ strings: this.strings,
310
+ templateString: this.templateString,
311
+ childNodes,
312
+ template,
313
+ parts,
314
+ }),
315
+ };
316
+ }
310
317
 
311
- return parts;
312
- }
318
+ return parts;
319
+ }
313
320
 
314
- /**
315
- * find interpolations in the given template for nodes and attributes and
316
- * return a string with placeholders as either comment nodes or named attributes.
317
- * @returns {string} template with tagged placeholders for values
318
- */
319
- get templateString() {
320
- return createTemplateString(this.strings);
321
- }
321
+ /**
322
+ * find interpolations in the given template for nodes and attributes and
323
+ * return a string with placeholders as either comment nodes or named attributes.
324
+ * @returns {string} template with tagged placeholders for values
325
+ */
326
+ get templateString() {
327
+ return createTemplateString(this.strings);
328
+ }
322
329
 
323
- toString() {
324
- let updates = parsedUpdates.get(this.strings);
330
+ toString() {
331
+ let updates = parsedUpdates.get(this.strings);
325
332
 
326
- if (!updates) {
327
- updates = this.parse(this.strings);
328
- parsedUpdates.set(this.strings, updates);
329
- }
333
+ if (!updates) {
334
+ updates = this.parse(this.strings);
335
+ parsedUpdates.set(this.strings, updates);
336
+ }
330
337
 
331
- return this.values.length ? this.values.map((value, index) => updates[index](value)).join('') : updates[0]();
332
- }
338
+ return this.values.length ? this.values.map((value, index) => updates[index](value)).join('') : updates[0]();
339
+ }
333
340
  }