@webtides/element-js 1.0.0 → 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 +140 -90
  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 +631 -631
  23. package/src/StyledElement.js +104 -104
  24. package/src/TemplateElement.js +53 -53
  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 +270 -270
  31. package/src/dom-parts/directives.js +105 -48
  32. package/src/dom-parts/html.js +1 -1
  33. package/src/dom-parts/render.js +6 -6
  34. package/src/util/AttributeParser.js +47 -47
  35. package/src/util/DOMHelper.js +190 -190
  36. package/src/util/Directive.js +31 -31
  37. package/src/util/GlobalStylesStore.js +56 -53
  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
@@ -22,50 +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
- // 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
- }
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
+ }
39
39
 
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 `
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
54
  ${attributesString.includes('\x01') ? attributeParts : ''}\n
55
55
  <${elementTagWithAttributes}>\n
56
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();
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();
69
69
  };
70
70
 
71
71
  /**
@@ -75,9 +75,9 @@ export const createTemplateString = (templateStrings, attributePlaceholders = ''
75
75
  * @return {String}
76
76
  */
77
77
  const attribute = (name, value, isSingleValue = true) => {
78
- return ` ${name}="${encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value)}${
79
- isSingleValue ? '"' : ''
80
- }`;
78
+ return ` ${name}="${encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value)}${
79
+ isSingleValue ? '"' : ''
80
+ }`;
81
81
  };
82
82
 
83
83
  /**
@@ -85,30 +85,30 @@ const attribute = (name, value, isSingleValue = true) => {
85
85
  * @return {String}
86
86
  */
87
87
  const getValue = (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));
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));
112
112
  };
113
113
 
114
114
  /** @type {Map<TemplateStringsArray, *[]>} */
@@ -118,223 +118,223 @@ const parsedUpdates = new WeakMap();
118
118
  const templateParts = new WeakMap();
119
119
 
120
120
  export class TemplateResult {
121
- /**
122
- * @param {TemplateStringsArray} strings
123
- * @param {any[]} values
124
- */
125
- constructor(strings, ...values) {
126
- this.strings = strings;
127
- this.values = values;
128
- }
121
+ /**
122
+ * @param {TemplateStringsArray} strings
123
+ * @param {any[]} values
124
+ */
125
+ constructor(strings, ...values) {
126
+ this.strings = strings;
127
+ this.values = values;
128
+ }
129
129
 
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');
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');
140
140
 
141
- serverSideRendered = startNode !== undefined;
141
+ serverSideRendered = startNode !== undefined;
142
142
 
143
- templatePart = new TemplatePart(startNode, this);
144
- templateParts.set(domNode, templatePart);
143
+ templatePart = new TemplatePart(startNode, this);
144
+ templateParts.set(domNode, templatePart);
145
145
 
146
- if (!serverSideRendered) {
147
- domNode.replaceChildren(...templatePart.childNodes);
148
- }
149
- } else {
150
- templatePart.update(this);
151
- }
152
- }
146
+ if (!serverSideRendered) {
147
+ domNode.replaceChildren(...templatePart.childNodes);
148
+ }
149
+ } else {
150
+ templatePart.update(this);
151
+ }
152
+ }
153
153
 
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
- }
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
+ }
229
229
 
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
- }
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
+ }
240
240
 
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
- }
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
+ }
250
250
 
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();
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();
261
261
 
262
- const treeWalker = globalThis.document?.createTreeWalker(template, 128);
263
- let node = treeWalker.nextNode(); //to skip the root template-part
264
- const parts = [];
262
+ const treeWalker = globalThis.document?.createTreeWalker(template, 128);
263
+ let node = treeWalker.nextNode(); //to skip the root template-part
264
+ const parts = [];
265
265
 
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
- }
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
+ }
301
301
 
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
- }
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
+ }
317
317
 
318
- return parts;
319
- }
318
+ return parts;
319
+ }
320
320
 
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
- }
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
+ }
329
329
 
330
- toString() {
331
- let updates = parsedUpdates.get(this.strings);
330
+ toString() {
331
+ let updates = parsedUpdates.get(this.strings);
332
332
 
333
- if (!updates) {
334
- updates = this.parse(this.strings);
335
- parsedUpdates.set(this.strings, updates);
336
- }
333
+ if (!updates) {
334
+ updates = this.parse(this.strings);
335
+ parsedUpdates.set(this.strings, updates);
336
+ }
337
337
 
338
- return this.values.length ? this.values.map((value, index) => updates[index](value)).join('') : updates[0]();
339
- }
338
+ return this.values.length ? this.values.map((value, index) => updates[index](value)).join('') : updates[0]();
339
+ }
340
340
  }