@webtides/element-js 1.0.0-alpha.1 → 1.0.0-alpha.11

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 (43) hide show
  1. package/docs/README.md +174 -0
  2. package/docs/concepts/classes.md +167 -0
  3. package/docs/concepts/hierarchy.md +61 -0
  4. package/docs/concepts/lifecycle.md +45 -0
  5. package/docs/concepts/reactivity.md +14 -0
  6. package/docs/features/computed-properties.md +28 -0
  7. package/docs/features/dependency-injection-context-protocol.md +132 -0
  8. package/docs/features/events.md +155 -0
  9. package/docs/features/global-styles.md +131 -0
  10. package/docs/features/methods.md +34 -0
  11. package/docs/features/properties.md +56 -0
  12. package/docs/features/refs.md +47 -0
  13. package/docs/features/renderless.md +70 -0
  14. package/docs/features/store.md +143 -0
  15. package/docs/features/styles.md +169 -0
  16. package/docs/features/templates.md +308 -0
  17. package/docs/features/watch.md +54 -0
  18. package/docs/getting-started/upgrade-to-v1.md +243 -0
  19. package/docs/guide/guide.md +112 -0
  20. package/index.js +9 -2
  21. package/package.json +15 -19
  22. package/src/BaseElement.js +206 -42
  23. package/src/StyledElement.js +67 -48
  24. package/src/TemplateElement.js +32 -7
  25. package/src/dom-parts/AttributePart.js +16 -9
  26. package/src/dom-parts/ChildNodePart.js +8 -22
  27. package/src/dom-parts/NodePart.js +1 -0
  28. package/src/dom-parts/TemplatePart.js +19 -25
  29. package/src/dom-parts/TemplateResult.js +46 -35
  30. package/src/dom-parts/directives.js +43 -16
  31. package/src/dom-parts/html.js +1 -1
  32. package/src/dom-parts/render.js +6 -1
  33. package/src/util/AttributeParser.js +54 -21
  34. package/src/util/DOMHelper.js +63 -14
  35. package/src/util/Directive.js +21 -1
  36. package/src/util/GlobalStylesStore.js +66 -0
  37. package/src/util/SerializeStateHelper.js +103 -0
  38. package/src/util/Store.js +111 -15
  39. package/src/util/defineElement.js +2 -2
  40. package/src/util/toString.js +2 -2
  41. package/dist/index.js +0 -1330
  42. package/dist/index.js.map +0 -7
  43. package/src/util/i18n.js +0 -20
@@ -12,13 +12,13 @@ const removeChildNodes = (node) => {
12
12
  const firstChild = Array.isArray(node)
13
13
  ? node[0]
14
14
  : node instanceof TemplatePart
15
- ? node.childNodes[0]
16
- : node.firstChild;
15
+ ? node.childNodes[0]
16
+ : node.firstChild;
17
17
  const lastChild = Array.isArray(node)
18
18
  ? node[node.length - 1]
19
19
  : node instanceof TemplatePart
20
- ? node.childNodes[node.childNodes.length - 1]
21
- : node.lastChild;
20
+ ? node.childNodes[node.childNodes.length - 1]
21
+ : node.lastChild;
22
22
  range.setStartAfter(firstChild);
23
23
  range.setEndAfter(lastChild);
24
24
  range.deleteContents();
@@ -75,28 +75,12 @@ const diffChildNodes = function (parentNode, domChildNodes, templateChildNodes,
75
75
  }
76
76
 
77
77
  // If DOM node is equal to the template node, don't do anything
78
- // if (domChildNode.isEqualNode(templateChildNode)) {
79
78
  if (domChildNode === templateChildNode) {
80
79
  continue;
81
80
  }
82
81
 
83
- // If node types are not the same, replace the DOM node with the template node
84
- if (templateChildNode.nodeType !== domChildNode.nodeType) {
85
- parentNode.replaceChild(templateChildNode, domChildNode);
86
- continue;
87
- }
88
-
89
- // If the element tag names are not the same, replace the DOM node with the template node
90
- if (templateChildNode.nodeType === ELEMENT_NODE && templateChildNode.tagName !== domChildNode.tagName) {
91
- parentNode.replaceChild(templateChildNode, domChildNode);
92
- continue;
93
- }
94
-
95
- // If the node is an SVG element, don't even think about diffing it, just replace it
96
- if (templateChildNode.nodeType === ELEMENT_NODE && templateChildNode.tagName === 'SVG') {
97
- parentNode.replaceChild(templateChildNode, domChildNode);
98
- continue;
99
- }
82
+ // Everything else is somehow different and can be replaced
83
+ parentNode.replaceChild(templateChildNode, domChildNode);
100
84
  }
101
85
  return templateChildNodes;
102
86
  };
@@ -212,6 +196,8 @@ export class ChildNodePart extends Part {
212
196
 
213
197
  let serverSideRendered = false;
214
198
  if (startNode) {
199
+ startNode.__part = this; // add Part to comment node for debugging in the browser
200
+
215
201
  const placeholder = startNode.data;
216
202
  const childNodes = [startNode];
217
203
  let childNode = startNode.nextSibling;
@@ -22,6 +22,7 @@ export class NodePart extends Part {
22
22
  );
23
23
  }
24
24
  super();
25
+ node.__part = this; // add Part to comment node for debugging in the browser
25
26
  this.node = node.nextElementSibling;
26
27
  const { directiveClass, values } = initialValue;
27
28
  this.directive = new directiveClass(node.nextElementSibling);
@@ -34,6 +34,8 @@ export class TemplatePart extends Part {
34
34
 
35
35
  let serverSideRendered = false;
36
36
  if (startNode) {
37
+ startNode.__part = this; // add Part to comment node for debugging in the browser
38
+
37
39
  const placeholder = startNode.data;
38
40
  const childNodes = [startNode];
39
41
  let childNode = startNode.nextSibling;
@@ -99,32 +101,24 @@ export class TemplatePart extends Part {
99
101
  partsCache.set(templateResult.strings, parts);
100
102
  }
101
103
 
102
- /** @type AttributePart */
103
- let previousAttributePart = undefined;
104
- this.parts = parts.map((part, index) => {
105
- // We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
106
- const node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
107
-
108
- if (part.type === 'node') {
109
- return new ChildNodePart(node, templateResult.values[index]);
110
- // TODO: the nodes in the parts array also have nested parts information... could we start creating the nested parts here as well?
111
- }
112
- if (part.type === 'attribute') {
113
- // If we have multiple attribute parts with the same name, it means we have multiple
114
- // interpolations inside that attribute. Instead of creating a new part, we will return the same
115
- // as before and let it defer the update until the last interpolation gets updated
116
- if (previousAttributePart && previousAttributePart.name === part.name) {
117
- previousAttributePart.addNode(node);
118
- return previousAttributePart;
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]);
119
113
  }
120
- return (previousAttributePart = new AttributePart(node, part.name, part.initialValue));
121
- }
122
- if (part.type === 'directive') {
123
- return new NodePart(node, templateResult.values[index]);
124
- }
125
-
126
- throw `cannot map part: ${part}`;
127
- });
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
+ });
128
122
  this.strings = templateResult.strings;
129
123
  }
130
124
  }
@@ -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
 
@@ -12,7 +12,7 @@ const partPositions = /[\x01\x02]/g;
12
12
  // \x03 COMMENT.ATTRIBUTE_TOKEN
13
13
  // \x04 Node.ATTRIBUTE_TOKEN
14
14
 
15
- const interpolation = new RegExp(`(<!--dom-part-(\\d+)--><!--/dom-part-(\\d+)-->|(\\S\\w+)="\x04(")?|\x04)`, 'g');
15
+ const interpolation = new RegExp(`(<!--dom-part-(\\d+)--><!--/dom-part-(\\d+)-->|(\\S[-\\w]+)="\x04(")?|\x04)`, 'g');
16
16
 
17
17
  /**
18
18
  * Given a template, find part positions as both nodes and attributes and
@@ -32,7 +32,7 @@ export const createTemplateString = (templateStrings, attributePlaceholders = ''
32
32
  if (trailingSlash.length) elementTagWithAttributes += voidElements.test(name) ? ' /' : '></' + name;
33
33
  // collect all attribute parts so that we can place matching comment nodes
34
34
  const attributeParts = attributesString.replace(attributes, (attribute, name, valueWithQuotes, directive) => {
35
- if (directive) {
35
+ if (directive && directive === '\x01') {
36
36
  return `<!--\x02$-->`;
37
37
  }
38
38
 
@@ -95,10 +95,16 @@ const getValue = (value) => {
95
95
  return String(value);
96
96
  case 'object':
97
97
  switch (true) {
98
+ case value === null:
99
+ return '';
98
100
  case Array.isArray(value):
99
101
  return value.map(getValue).join('');
100
102
  case value instanceof TemplateResult:
101
103
  return value.toString();
104
+ case value.__unsafeHTML:
105
+ return value.string;
106
+ default:
107
+ console.log('Cannot getValue for', value);
102
108
  }
103
109
  break;
104
110
  case 'function':
@@ -213,7 +219,8 @@ export class TemplateResult {
213
219
  // TODO: we currently cannot distinguish between NodeParts and AttributeParts before
214
220
  if (isObjectLike(value) && value.directiveClass) {
215
221
  // NodePart
216
- // result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
222
+ const directive = new value.directiveClass();
223
+ result += directive.stringify(...value.values);
217
224
  } else if (value != null) {
218
225
  result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
219
226
  }
@@ -255,40 +262,44 @@ export class TemplateResult {
255
262
  const template = range.cloneContents();
256
263
 
257
264
  const treeWalker = globalThis.document?.createTreeWalker(template, 128);
258
- let node = treeWalker.currentNode;
265
+ let node = treeWalker.nextNode(); //to skip the root template-part
266
+ const parts = [];
259
267
 
260
- // this is kinda freaky because the tree walker is shared throughout the recursion - but YOLO
261
- const getParts = () => {
262
- const parts = [];
263
- let currentPart = undefined;
264
- // search for parts through numbered comment nodes with placeholders
265
- while ((node = treeWalker.nextNode())) {
266
- if (/^template-part$/.test(node.data) && currentPart) {
267
- // start recursion -> add nested parts to previous part
268
- currentPart.parts = getParts();
269
- }
270
- if (/^\/template-part$/.test(node.data)) {
271
- // end recursion
272
- return parts;
273
- }
274
- if (/^dom-part-\d+$/.test(node.data)) {
275
- parts.push((currentPart = { type: 'node', path: getNodePath(node), parts: [] }));
276
- }
277
- if (/^dom-part-\d+:/.test(node.data)) {
278
- const [_, attribute] = node.data.split(':');
279
- const [name, initialValue] = attribute.split('=');
280
- currentPart = undefined;
281
- parts.push({ type: 'attribute', path: getNodePath(node), name: name, initialValue });
282
- }
283
- if (/^dom-part-\d+\$/.test(node.data)) {
284
- currentPart = undefined;
285
- parts.push({ type: 'directive', path: getNodePath(node) });
268
+ let nestedLevel = 0;
269
+ // search for parts through numbered comment nodes with placeholders
270
+ while ((node = treeWalker.nextNode())) {
271
+ if (/^template-part$/.test(node.data)) {
272
+ nestedLevel++;
273
+ continue;
274
+ }
275
+ if (/^\/template-part$/.test(node.data)) {
276
+ if (nestedLevel > 0) {
277
+ nestedLevel--;
286
278
  }
279
+ continue;
287
280
  }
288
- return parts;
289
- };
290
-
291
- const parts = getParts();
281
+ if (nestedLevel > 0) {
282
+ continue;
283
+ }
284
+ if (/^dom-part-\d+$/.test(node.data)) {
285
+ parts.push({ type: 'node', path: getNodePath(node) });
286
+ continue;
287
+ }
288
+ if (/^dom-part-\d+:/.test(node.data)) {
289
+ const [_, ...attribute] = node.data.split(':');
290
+ const [name, ...initialValue] = attribute.join(':').split('=');
291
+ parts.push({
292
+ type: 'attribute',
293
+ path: getNodePath(node),
294
+ name: name,
295
+ initialValue: initialValue.join('='),
296
+ });
297
+ continue;
298
+ }
299
+ if (/^dom-part-\d+\$/.test(node.data)) {
300
+ parts.push({ type: 'directive', path: getNodePath(node) });
301
+ }
302
+ }
292
303
 
293
304
  // We couldn't correctly parse parts from the template
294
305
  if (parts.length !== this.strings.length - 1) {
@@ -1,6 +1,7 @@
1
1
  import { camelToDash, decodeAttribute, encodeAttribute } from '../util/AttributeParser.js';
2
- import { TemplateResult } from './TemplateResult';
3
- import { convertStringToHTML } from '../util/DOMHelper';
2
+ import { TemplateResult } from './TemplateResult.js';
3
+ import { convertStringToHTML, convertStringToTemplate } from '../util/DOMHelper.js';
4
+ import { defineDirective, Directive } from '../util/Directive.js';
4
5
 
5
6
  /**
6
7
  * Maps a list of classes to an element from an object.
@@ -53,32 +54,58 @@ const choose = (value, cases, defaultCase) => {
53
54
  };
54
55
 
55
56
  /**
56
- * Renders a given string as HTML instead of text
57
+ * Renders a given string as HTML instead of text or as unescaped string in SSR mode
57
58
  * @param {string} string
58
- * @returns {function(): Node}
59
+ * @returns {(function(): Node) | { __unsafeHTML: true, string: string }}
59
60
  */
60
61
  const unsafeHTML = (string) => {
61
- // TODO: write tests for this...
62
- // I mean should the string always have a single root element? Or could it also handle multiple root elements?!
63
- return () => convertStringToHTML(string).firstChild;
62
+ if (!globalThis.DOMParser) {
63
+ return { __unsafeHTML: true, string };
64
+ }
65
+ const node = convertStringToHTML(string).firstChild;
66
+ const importedNode = globalThis.document?.importNode(node, true);
67
+ return () => importedNode;
64
68
  };
65
69
 
66
70
  /**
67
71
  * Renders multiple attributes as key="value" pairs from a map of attributes
68
- * @param {Object.<string, any>} attributes
69
- * @returns {function(): string}
70
72
  */
71
- const spreadAttributes = (attributes) => {
72
- return () => {
73
+ export class SpreadAttributesDirective extends Directive {
74
+ /**
75
+ * @param {Object.<string, any>} attributes
76
+ */
77
+ update(attributes) {
78
+ // TODO: what if the attribute was already there..?!
79
+ // TODO: what if the attribute was there from a previous render/update, but not anymore?!
80
+ for (const name of Object.keys(attributes)) {
81
+ const value = attributes[name];
82
+ this.node.setAttribute(
83
+ camelToDash(name),
84
+ encodeAttribute(typeof value !== 'string' ? JSON.stringify(value) : value),
85
+ );
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {Object.<string, any>} attributes
91
+ * @returns {string}
92
+ */
93
+ stringify(attributes) {
73
94
  return Object.keys(attributes)
74
- .map((key) => {
75
- let value = attributes[key];
76
- return `${camelToDash(key)}='${encodeAttribute(
95
+ .map((name) => {
96
+ let value = attributes[name];
97
+ return `${camelToDash(name)}='${encodeAttribute(
77
98
  typeof value !== 'string' ? JSON.stringify(value) : value,
78
99
  )}'`;
79
100
  })
80
101
  .join(' ');
81
- };
82
- };
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Renders multiple attributes as key="value" pairs from a map of attributes
107
+ * @param {Object.<string, any>} attributes
108
+ */
109
+ const spreadAttributes = defineDirective(SpreadAttributesDirective);
83
110
 
84
111
  export { classMap, styleMap, when, choose, unsafeHTML, spreadAttributes };
@@ -1,4 +1,4 @@
1
- import { TemplateResult } from './TemplateResult';
1
+ import { TemplateResult } from './TemplateResult.js';
2
2
 
3
3
  /**
4
4
  * @param {TemplateStringsArray} strings
@@ -4,7 +4,12 @@
4
4
  * @param {Element} domNode
5
5
  */
6
6
  const render = (template, domNode) => {
7
- template.renderInto(domNode);
7
+ if (typeof template === 'string') {
8
+ // just a plain string (or literal)
9
+ domNode.innerHTML = template;
10
+ } else {
11
+ template.renderInto(domNode);
12
+ }
8
13
  };
9
14
 
10
15
  export { render };
@@ -1,23 +1,48 @@
1
+ /**
2
+ * Tests if a value is of type `object`
3
+ * @param {any} value
4
+ * @returns {boolean}
5
+ */
1
6
  export function isObjectLike(value) {
2
7
  return typeof value == 'object' && value !== null;
3
8
  }
4
9
 
5
- export function isJSON(str) {
10
+ /**
11
+ * Tests if a value can be parsed as JSON
12
+ * @param {any} value
13
+ * @returns {boolean}
14
+ */
15
+ export function isJSON(value) {
6
16
  try {
7
- return JSON.parse(str) && !!str;
17
+ return JSON.parse(value) && !!value;
8
18
  } catch (e) {
9
19
  return false;
10
20
  }
11
21
  }
12
22
 
23
+ /**
24
+ * Tests if a value is `boolean`
25
+ * @param {any} value
26
+ * @returns {boolean}
27
+ */
13
28
  export function isBoolean(value) {
14
29
  return value === 'true' || value === 'false';
15
30
  }
16
31
 
32
+ /**
33
+ * Parses a value to `boolean`
34
+ * @param {any} value
35
+ * @returns {boolean}
36
+ */
17
37
  export function parseBoolean(value) {
18
38
  return value === 'true';
19
39
  }
20
40
 
41
+ /**
42
+ * Tests if a value is of type `string`
43
+ * @param {any} value
44
+ * @returns {boolean}
45
+ */
21
46
  export function isString(value) {
22
47
  return (
23
48
  typeof value === 'string' ||
@@ -25,26 +50,40 @@ export function isString(value) {
25
50
  );
26
51
  }
27
52
 
53
+ /**
54
+ * Tests if a value is of type `number`
55
+ * @param {any} value
56
+ * @returns {boolean}
57
+ */
28
58
  export function isNumber(value) {
29
59
  return new RegExp('^-?(0|0\\.\\d+|[1-9]\\d*(\\.\\d+)?)$').test(value);
30
60
  }
31
61
 
62
+ /**
63
+ * Tests if a value is of type `NaN`
64
+ * @param {any} value
65
+ * @returns {boolean}
66
+ */
32
67
  export function isNaN(value) {
33
68
  return Number.isNaN(value);
34
69
  }
35
70
 
36
71
  /**
37
- * deep compare two values primitive or complex
38
- *
39
- * @param valueA
40
- * @param valueB
72
+ * Compare two values (deep). It compares primitive or complex values with JSON stringify.
73
+ * @param {any} valueA
74
+ * @param {any} valueB
41
75
  * @returns {boolean}
42
76
  */
43
-
44
77
  export function deepEquals(valueA, valueB) {
45
78
  return JSON.stringify(valueA) === JSON.stringify(valueB);
46
79
  }
47
80
 
81
+ /**
82
+ * Parses an attribute value that comes in as a `string` to its corresponding type
83
+ * @param {string} value
84
+ * @param {PropertyOptions} options
85
+ * @returns {string | number | boolean | object | array}
86
+ */
48
87
  export function parseAttribute(value, options = {}) {
49
88
  if (options.parse === false || !isString(value)) {
50
89
  // no-op
@@ -67,7 +106,8 @@ export function parseAttribute(value, options = {}) {
67
106
 
68
107
  /**
69
108
  * Replaces dashed-expression (i.e. some-value) to a camel-cased expression (i.e. someValue)
70
- * @returns string
109
+ * @param {string} string
110
+ * @returns {string}
71
111
  */
72
112
  export function dashToCamel(string) {
73
113
  if (string.indexOf('-') === -1) return string;
@@ -77,17 +117,16 @@ export function dashToCamel(string) {
77
117
 
78
118
  /**
79
119
  * Replaces camel-cased expression (i.e. someValue) to a dashed-expression (i.e. some-value)
80
- * @returns string
120
+ * @param {string} string
121
+ * @returns {string}
81
122
  */
82
123
  export function camelToDash(string) {
83
124
  return string.replace(/([A-Z])/g, '-$1').toLowerCase();
84
125
  }
85
126
 
86
127
  /**
87
- * Decodes an attribute. Can only be encoded with the `encodeAttribute` method which
88
- * is exported from this file.
89
- *
90
- * @param attribute
128
+ * Decodes an attribute.
129
+ * @param {string} attribute
91
130
  * @returns {string}
92
131
  */
93
132
  export function decodeAttribute(attribute) {
@@ -99,14 +138,8 @@ export function decodeAttribute(attribute) {
99
138
  }
100
139
 
101
140
  /**
102
- * Encodes an attribute. Can only be decoded with the `decodeAttribute` method which
103
- * is exported from this file.
104
- *
105
- * This does not use the standard way to encode an attribute, as the dom parser
106
- * that renders the html would understand and automatically decode it. Which results
107
- * in non sanitized html output.
108
- *
109
- * @param attribute
141
+ * Encodes an attribute.
142
+ * @param {string} attribute
110
143
  * @returns {string}
111
144
  */
112
145
  export function encodeAttribute(attribute) {
@@ -6,10 +6,12 @@ export const ATTRIBUTE_NODE = 2;
6
6
  export const TEXT_NODE = 3;
7
7
  /* A Comment node, such as <!-- … --> */
8
8
  export const COMMENT_NODE = 8;
9
+ /* A Document Fragment, a document without a parent */
10
+ export const DOCUMENT_FRAGMENT_NODE = 11;
9
11
 
10
12
  /**
11
- * Gets list of (childNode) indexes for the given node
12
- * @param node
13
+ * Gets a list of (childNode) indexes for the given node
14
+ * @param {Node} node
13
15
  * @return {number[]}
14
16
  */
15
17
  export const getNodePath = (node) => {
@@ -23,6 +25,11 @@ export const getNodePath = (node) => {
23
25
  return path;
24
26
  };
25
27
 
28
+ /**
29
+ * Get a list of all parent nodes up the DOM tree
30
+ * @param {Node} domNode
31
+ * @returns {Node[]}
32
+ */
26
33
  export function getAllParentNodes(domNode) {
27
34
  let node = domNode.parentElement;
28
35
  let parents = [];
@@ -34,7 +41,12 @@ export function getAllParentNodes(domNode) {
34
41
  return parents;
35
42
  }
36
43
 
37
- // https://stackoverflow.com/questions/27453617/how-can-i-tell-if-an-element-is-in-a-shadow-dom
44
+ /**
45
+ * Gets the parent/root element of a shadow root or the global document body if not in shadow root.
46
+ * https://stackoverflow.com/questions/27453617/how-can-i-tell-if-an-element-is-in-a-shadow-dom
47
+ * @param {Node} element
48
+ * @returns {ShadowRoot | HTMLElement}
49
+ */
38
50
  export function getShadowParentOrBody(element) {
39
51
  if (element instanceof ShadowRoot) {
40
52
  return element;
@@ -50,6 +62,11 @@ export function getShadowParentOrBody(element) {
50
62
 
51
63
  // TODO: add function for getClosestParentOfNodeType('custom-element')
52
64
 
65
+ /**
66
+ * Get the closest/first parent node that is a custom element
67
+ * @param {Node} domNode
68
+ * @returns {Node}
69
+ */
53
70
  export function getClosestParentCustomElementNode(domNode) {
54
71
  const customElementParents = getAllParentNodes(domNode).filter((node) => {
55
72
  return node.localName && node.localName.indexOf('-') !== -1;
@@ -58,19 +75,51 @@ export function getClosestParentCustomElementNode(domNode) {
58
75
  return customElementParents.pop();
59
76
  }
60
77
 
78
+ /**
79
+ * Get all
80
+ * @param {Node} domNode
81
+ * @param {{prototype: HTMLElement, new()}} instanceOfFilter
82
+ * @return {Node[]}
83
+ */
84
+ export function getAllElementChildren(domNode, instanceOfFilter = HTMLElement) {
85
+ const nodeIterator = document.createNodeIterator(domNode, NodeFilter.SHOW_ELEMENT, (node) =>
86
+ node.nodeName.includes('-') && node instanceof instanceOfFilter
87
+ ? NodeFilter.FILTER_ACCEPT
88
+ : NodeFilter.FILTER_REJECT,
89
+ );
90
+ const customElements = [];
91
+ let currentNode;
92
+ while ((currentNode = nodeIterator.nextNode())) {
93
+ customElements.push(currentNode);
94
+ }
95
+ return customElements;
96
+ }
97
+
98
+ /**
99
+ * Checks whether two nodes are of the same node type
100
+ * @param {Node} nodeA
101
+ * @param {Node} nodeB
102
+ * @returns {boolean}
103
+ */
61
104
  export function isOfSameNodeType(nodeA, nodeB) {
62
105
  if (!nodeA && !nodeA.localName) return false;
63
106
  if (!nodeB && !nodeB.localName) return false;
64
107
  return nodeA.localName === nodeB.localName;
65
108
  }
66
109
 
67
- export const supportsAdoptingStyleSheets = () =>
68
- 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype;
69
-
70
- // for IE11 we are using the ShadyDOM Polyfill. With the polyfill we cannot append stylesheets to the shadowRoot
71
- export const supportsAppendingStyleSheets = !window.ShadyDOM;
110
+ /**
111
+ * Checks if adopting style sheets is supported in browser
112
+ * @returns {boolean}
113
+ */
114
+ export const supportsAdoptingStyleSheets = () => {
115
+ return 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype;
116
+ };
72
117
 
73
- // better hasChildNodes check because the native check will also count for empty whitespace as child nodes... :(
118
+ /**
119
+ * Better implementation of hasChildNodes check because the native check will also count for empty whitespace as child nodes... :(
120
+ * @param {Node} node
121
+ * @returns {boolean}
122
+ */
74
123
  export const hasChildNodes = (node) => {
75
124
  const childNodesLength = node.childNodes.length;
76
125
 
@@ -86,7 +135,7 @@ export const hasChildNodes = (node) => {
86
135
  };
87
136
 
88
137
  /**
89
- * Parses the given template string and returns a real DOM element.
138
+ * Parses the given string as HTML template and returns a real DOM element
90
139
  * @param {string} template
91
140
  * @returns {HTMLElement}
92
141
  */
@@ -97,8 +146,8 @@ export const convertStringToHTML = (template) => {
97
146
  };
98
147
 
99
148
  /**
100
- * Parses the given string and returns a DocumentFragment
101
- * which contains the DOM subtree representing the <template> element's template contents.
149
+ * Parses the given string and returns a DocumentFragment which contains the DOM subtree
150
+ * representing the <template> element's template contents.
102
151
  * @param {string} string
103
152
  * @return {DocumentFragment}
104
153
  */
@@ -111,8 +160,8 @@ export const convertStringToTemplate = (string) => {
111
160
 
112
161
  /**
113
162
  * Diff the attributes for a live DOM element against a template DOM element
114
- * @param {Element} templateElement The new template
115
- * @param {Element} domElement The existing DOM node
163
+ * @param {Element} templateElement - The new template
164
+ * @param {Element} domElement - The existing DOM node
116
165
  */
117
166
  export const diffAttributes = function (templateElement, domElement) {
118
167
  // remove or update previous attributes
@@ -2,6 +2,9 @@ export class Directive {
2
2
  /** @type {Element} */
3
3
  node = undefined;
4
4
 
5
+ /**
6
+ * @param {Node} node
7
+ */
5
8
  constructor(node) {
6
9
  this.node = node;
7
10
  }
@@ -10,6 +13,16 @@ export class Directive {
10
13
  * @abstract
11
14
  */
12
15
  update() {}
16
+
17
+ // TODO: this is somewhat custom... maybe we could set the initial values in the constructor and then simply override the default toString() method?!
18
+ /**
19
+ * Implement the stringify method and return a string for rendering the directive in SSR mode
20
+ * @param {...any} values
21
+ * @returns {string}
22
+ */
23
+ stringify(...values) {
24
+ return '';
25
+ }
13
26
  }
14
27
 
15
28
  /**
@@ -17,6 +30,13 @@ export class Directive {
17
30
  */
18
31
  export const defineDirective = (directiveClass) => {
19
32
  return (...values) => {
20
- return { directiveClass, values };
33
+ return {
34
+ directiveClass,
35
+ values,
36
+ toString: () => {
37
+ const directive = new directiveClass();
38
+ return directive.stringify(...values);
39
+ },
40
+ };
21
41
  };
22
42
  };