@webtides/element-js 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +147 -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 +632 -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 +63 -53
  38. package/src/util/SerializeStateHelper.js +56 -56
  39. package/src/util/Store.js +175 -174
  40. package/src/util/crypto.js +13 -0
  41. package/src/util/defineElement.js +5 -5
  42. package/src/util/toString.js +5 -5
@@ -12,110 +12,110 @@ import { globalStylesStore } from './util/GlobalStylesStore.js';
12
12
  */
13
13
 
14
14
  class StyledElement extends BaseElement {
15
- /**
16
- * @param {StyledElementOptions} options
17
- */
18
- constructor(options) {
19
- super({
20
- deferUpdate: false,
21
- shadowRender: false,
22
- styles: [],
23
- adoptGlobalStyles: true,
24
- ...options,
25
- });
26
- this._styles = [...this._options.styles, ...this.styles()];
27
- }
28
-
29
- /**
30
- * Overrides the `connectedCallback` to adopt optional styles when the element is connected
31
- */
32
- connectedCallback() {
33
- super.connectedCallback();
34
-
35
- if (!this.constructor['elementStyleSheets']) {
36
- this.constructor['elementStyleSheets'] = this._styles.map((style) => {
37
- const cssStyleSheet = new CSSStyleSheet();
38
- cssStyleSheet.replaceSync(style);
39
- return cssStyleSheet;
40
- });
41
- }
42
-
43
- if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
44
- // adopting does only make sense in shadow dom. Fall back to append for light elements
45
- this.adoptStyleSheets();
46
-
47
- if (this._options.adoptGlobalStyles !== false) {
48
- globalStylesStore.subscribe(() => {
49
- this.adoptStyleSheets();
50
- });
51
- }
52
- }
53
- }
54
-
55
- /**
56
- * The styles method is another way to return a list of styles to be adopted when the element is connected.
57
- * You can either provide a list in the constructor options or return it here.
58
- * @returns {string[]}
59
- */
60
- styles() {
61
- return [];
62
- }
63
-
64
- /**
65
- * Overrides the `update` method to adopt optional styles
66
- * @param {PropertyUpdateOptions} options
67
- */
68
- update(options) {
69
- // We cannot do this in connectedCallback() since the whole template will be overridden in update/render afterward
70
- if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
71
- this.appendStyleSheets();
72
- }
73
- super.update(options);
74
- }
75
-
76
- /**
77
- * Adopt stylesheets
78
- */
79
- adoptStyleSheets() {
80
- const adoptGlobalStyleSheets = this._options.shadowRender && this._options.adoptGlobalStyles !== false;
81
-
82
- this.getRoot().adoptedStyleSheets = [
83
- ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
84
- ...this.constructor['elementStyleSheets'],
85
- ];
86
- }
87
-
88
- /**
89
- * Custom polyfill for constructable stylesheets by appending styles to the end of an element
90
- */
91
- appendStyleSheets() {
92
- const parentDocument = getShadowParentOrBody(this.getRoot());
93
-
94
- const adoptGlobalStyleSheets =
95
- this._options.shadowRender &&
96
- this._options.adoptGlobalStyles !== false &&
97
- parentDocument !== globalThis.document.body;
98
-
99
- const appendableStyles = [
100
- ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
101
- ...this.constructor['elementStyleSheets'],
102
- ];
103
-
104
- appendableStyles.forEach((styleSheet, index) => {
105
- const identifier = this.tagName + index;
106
-
107
- // only append stylesheet if not already appended to shadowRoot or document
108
- if (!parentDocument.querySelector(`#${identifier}`)) {
109
- const styleElement = globalThis.document?.createElement('style');
110
- styleElement.id = identifier;
111
- styleElement.style.display = 'none';
112
- styleElement.textContent = Array.from(styleSheet.cssRules)
113
- .map((rule) => rule.cssText)
114
- .join('');
115
- parentDocument.appendChild(styleElement);
116
- }
117
- });
118
- }
15
+ /**
16
+ * @param {StyledElementOptions} options
17
+ */
18
+ constructor(options) {
19
+ super({
20
+ deferUpdate: false,
21
+ shadowRender: false,
22
+ styles: [],
23
+ adoptGlobalStyles: true,
24
+ ...options,
25
+ });
26
+ this._styles = [...this._options.styles, ...this.styles()];
27
+ }
28
+
29
+ /**
30
+ * Overrides the `connectedCallback` to adopt optional styles when the element is connected
31
+ */
32
+ connectedCallback() {
33
+ super.connectedCallback();
34
+
35
+ if (!this.constructor['elementStyleSheets']) {
36
+ this.constructor['elementStyleSheets'] = this._styles.map((style) => {
37
+ const cssStyleSheet = new CSSStyleSheet();
38
+ cssStyleSheet.replaceSync(style);
39
+ return cssStyleSheet;
40
+ });
41
+ }
42
+
43
+ if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
44
+ // adopting does only make sense in shadow dom. Fall back to append for light elements
45
+ this.adoptStyleSheets();
46
+
47
+ if (this._options.adoptGlobalStyles !== false) {
48
+ globalStylesStore.subscribe(() => {
49
+ this.adoptStyleSheets();
50
+ });
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * The styles method is another way to return a list of styles to be adopted when the element is connected.
57
+ * You can either provide a list in the constructor options or return it here.
58
+ * @returns {string[]}
59
+ */
60
+ styles() {
61
+ return [];
62
+ }
63
+
64
+ /**
65
+ * Overrides the `update` method to adopt optional styles
66
+ * @param {PropertyUpdateOptions} options
67
+ */
68
+ update(options) {
69
+ // We cannot do this in connectedCallback() since the whole template will be overridden in update/render afterward
70
+ if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
71
+ this.appendStyleSheets();
72
+ }
73
+ super.update(options);
74
+ }
75
+
76
+ /**
77
+ * Adopt stylesheets
78
+ */
79
+ adoptStyleSheets() {
80
+ const adoptGlobalStyleSheets = this._options.shadowRender && this._options.adoptGlobalStyles !== false;
81
+
82
+ this.getRoot().adoptedStyleSheets = [
83
+ ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
84
+ ...this.constructor['elementStyleSheets'],
85
+ ];
86
+ }
87
+
88
+ /**
89
+ * Custom polyfill for constructable stylesheets by appending styles to the end of an element
90
+ */
91
+ appendStyleSheets() {
92
+ const parentDocument = getShadowParentOrBody(this.getRoot());
93
+
94
+ const adoptGlobalStyleSheets =
95
+ this._options.shadowRender &&
96
+ this._options.adoptGlobalStyles !== false &&
97
+ parentDocument !== globalThis.document.body;
98
+
99
+ const appendableStyles = [
100
+ ...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
101
+ ...this.constructor['elementStyleSheets'],
102
+ ];
103
+
104
+ appendableStyles.forEach((styleSheet, index) => {
105
+ const identifier = this.tagName + index;
106
+
107
+ // only append stylesheet if not already appended to shadowRoot or document
108
+ if (!parentDocument.querySelector(`#${identifier}`)) {
109
+ const styleElement = globalThis.document?.createElement('style');
110
+ styleElement.id = identifier;
111
+ styleElement.style.display = 'none';
112
+ styleElement.textContent = Array.from(styleSheet.cssRules)
113
+ .map((rule) => rule.cssText)
114
+ .join('');
115
+ parentDocument.appendChild(styleElement);
116
+ }
117
+ });
118
+ }
119
119
  }
120
120
 
121
121
  export { StyledElement };
@@ -9,64 +9,64 @@ import { render } from './dom-parts/render.js';
9
9
  */
10
10
 
11
11
  class TemplateElement extends StyledElement {
12
- /**
13
- * @param {TemplateElementOptions} options
14
- */
15
- constructor(options) {
16
- super({
17
- deferUpdate: false,
18
- shadowRender: false,
19
- styles: [],
20
- adoptGlobalStyles: true,
21
- mutationObserverOptions: {
22
- childList: false,
23
- },
24
- ...options,
25
- });
26
- this._template = this._options.template;
27
- }
12
+ /**
13
+ * @param {TemplateElementOptions} options
14
+ */
15
+ constructor(options) {
16
+ super({
17
+ deferUpdate: false,
18
+ shadowRender: false,
19
+ styles: [],
20
+ adoptGlobalStyles: true,
21
+ mutationObserverOptions: {
22
+ childList: false,
23
+ },
24
+ ...options,
25
+ });
26
+ this._template = this._options.template;
27
+ }
28
28
 
29
- /**
30
- * Overrides the native `connectedCallback` of the HTMLElement to set up and initialize our element.
31
- * This will attach a shadow DOM if the element is supposed to render in shadow DOM.
32
- */
33
- connectedCallback() {
34
- if (this._options.shadowRender && !this.shadowRoot) this.attachShadow({ mode: 'open' });
35
- super.connectedCallback();
36
- }
29
+ /**
30
+ * Overrides the native `connectedCallback` of the HTMLElement to set up and initialize our element.
31
+ * This will attach a shadow DOM if the element is supposed to render in shadow DOM.
32
+ */
33
+ connectedCallback() {
34
+ if (this._options.shadowRender && !this.shadowRoot) this.attachShadow({ mode: 'open' });
35
+ super.connectedCallback();
36
+ }
37
37
 
38
- /**
39
- * The template method should be overridden in extending elements and return the template to be rendered to the root
40
- * @returns {TemplateResult | string}
41
- */
42
- template() {
43
- return html``;
44
- }
38
+ /**
39
+ * The template method should be overridden in extending elements and return the template to be rendered to the root
40
+ * @returns {TemplateResult | string}
41
+ */
42
+ template() {
43
+ return html``;
44
+ }
45
45
 
46
- /**
47
- * Override update method to render the template to the root
48
- * @param {PropertyUpdateOptions} options
49
- */
50
- update(options) {
51
- this.renderTemplate();
52
- super.update(options);
53
- }
46
+ /**
47
+ * Override update method to render the template to the root
48
+ * @param {PropertyUpdateOptions} options
49
+ */
50
+ update(options) {
51
+ this.renderTemplate();
52
+ super.update(options);
53
+ }
54
54
 
55
- /**
56
- * Render the template to the root
57
- */
58
- renderTemplate() {
59
- const template = this._template || this.template();
60
- render(template, this.getRoot());
61
- }
55
+ /**
56
+ * Render the template to the root
57
+ */
58
+ renderTemplate() {
59
+ const template = this._template || this.template();
60
+ render(template, this.getRoot());
61
+ }
62
62
 
63
- /**
64
- * Get the root element - either the element or the shadow root
65
- * @returns {ShadowRoot | HTMLElement}
66
- */
67
- getRoot() {
68
- return this.shadowRoot !== null ? this.shadowRoot : this;
69
- }
63
+ /**
64
+ * Get the root element - either the element or the shadow root
65
+ * @returns {ShadowRoot | HTMLElement}
66
+ */
67
+ getRoot() {
68
+ return this.shadowRoot !== null ? this.shadowRoot : this;
69
+ }
70
70
  }
71
71
 
72
72
  export { TemplateElement, html, render };
@@ -7,14 +7,14 @@ import { Part } from './Part.js';
7
7
  * @return {(function(*): void)|*}
8
8
  */
9
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
- };
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
18
  };
19
19
 
20
20
  /**
@@ -23,11 +23,11 @@ const processBooleanAttribute = (node, name, oldValue) => {
23
23
  * @return {(function(*): void)|*}
24
24
  */
25
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
- };
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
31
  };
32
32
 
33
33
  /**
@@ -36,18 +36,18 @@ const processPropertyAttribute = (node, name) => {
36
36
  * @return {(function(*): void)|*}
37
37
  */
38
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
- };
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
51
  };
52
52
 
53
53
  /**
@@ -56,26 +56,26 @@ const processEventAttribute = (node, name) => {
56
56
  * @return {(function(*): void)|*}
57
57
  */
58
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
- };
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
79
  };
80
80
 
81
81
  /** @type {Map<Element, AttributePart>} */
@@ -87,81 +87,81 @@ const attributePartsCache = new WeakMap();
87
87
  * @return {(function(*): void)|*}
88
88
  */
89
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);
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
107
  };
108
108
 
109
109
  /**
110
110
  * For updating a single attribute
111
111
  */
112
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
- }
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
167
  }