@webtides/element-js 0.4.3 → 0.6.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.
package/index.js CHANGED
@@ -4,5 +4,6 @@ import { TemplateElement, html } from './src/TemplateElement';
4
4
  import { toString } from './src/util/toString';
5
5
  import { defineElement } from './src/util/defineElement';
6
6
  import { i18n } from './src/util/i18n';
7
+ import { Store } from './src/util/Store';
7
8
 
8
- export { BaseElement, StyledElement, TemplateElement, html, toString, defineElement, i18n };
9
+ export { BaseElement, StyledElement, TemplateElement, Store, html, toString, defineElement, i18n };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "module": "index.js",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -18,7 +18,10 @@
18
18
  "./src/util/*": "./src/util/*.js",
19
19
  "./src/renderer/vanilla": "./src/renderer/vanilla/index.js",
20
20
  "./src/renderer/vanilla/TemplateElement": "./src/renderer/vanilla/TemplateElement.js",
21
- "./src/renderer/vanilla/util/*": "./src/renderer/vanilla/util/*.js"
21
+ "./src/renderer/vanilla/util/*": "./src/renderer/vanilla/util/*.js",
22
+ "./src/renderer/vanilla/util/directives.js": "./src/renderer/vanilla/util/directives.js",
23
+ "./src/renderer/vanilla/util/html.js": "./src/renderer/vanilla/util/html.js",
24
+ "./src/renderer/vanilla/util/render.js": "./src/renderer/vanilla/util/render.js"
22
25
  },
23
26
  "repository": "https://github.com/webtides/element-js.git",
24
27
  "author": "@webtides",
@@ -39,6 +42,7 @@
39
42
  },
40
43
  "devDependencies": {
41
44
  "@open-wc/testing": "^3.0.3",
45
+ "@web/dev-server": "^0.1.30",
42
46
  "@web/test-runner": "^0.13.26",
43
47
  "babel-eslint": "^10.1.0",
44
48
  "babel-plugin-module-resolver": "^4.0.0",
@@ -62,6 +66,7 @@
62
66
  "prepare": "esbuild index.js --outfile=dist/index.js --bundle --format=esm --sourcemap --target=chrome79,firefox68,safari12,edge79",
63
67
  "test": "web-test-runner test/**/*.test.{html,js} --node-resolve",
64
68
  "test:coverage": "web-test-runner test/**/*.test.{html,js} --node-resolve --coverage",
65
- "test:watch": "web-test-runner test/**/*.test.{html,js} --node-resolve --watch"
69
+ "test:watch": "web-test-runner test/**/*.test.{html,js} --node-resolve --watch",
70
+ "start": "web-dev-server --config web-dev-server.config.mjs"
66
71
  }
67
72
  }
@@ -1,5 +1,6 @@
1
1
  import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from './util/AttributeParser.js';
2
2
  import { getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
3
+ import { Store } from './util/Store.js';
3
4
 
4
5
  export { defineElement } from './util/defineElement';
5
6
  export { toString } from './util/toString';
@@ -98,6 +99,13 @@ class BaseElement extends HTMLElement {
98
99
  // remove observers
99
100
  if (this._mutationObserver) this._mutationObserver.disconnect();
100
101
 
102
+ // unsubscribe from stores
103
+ Object.values(this.properties()).forEach((property) => {
104
+ if (property instanceof Store) {
105
+ property.unsubscribe(this);
106
+ }
107
+ });
108
+
101
109
  this.triggerHook('disconnected');
102
110
  }
103
111
 
@@ -198,6 +206,10 @@ class BaseElement extends HTMLElement {
198
206
  * Will trigger update() when a property was changed
199
207
  */
200
208
  defineProperty(property, value, reflectAttribute = false) {
209
+ if (value instanceof Store) {
210
+ value.subscribe(this);
211
+ }
212
+
201
213
  if (this._state.hasOwnProperty(property)) {
202
214
  // property has already been defined as an attribute nothing to do here
203
215
  return;
@@ -0,0 +1,78 @@
1
+ import { camelToDash, decodeAttribute } from '../../../util/AttributeParser.js';
2
+ import { Part } from './html.js';
3
+
4
+ /**
5
+ * Maps a list of classes to an element from an object.
6
+ * The keys will become the classes when the value is truthy.
7
+ * @param {Object.<string, string | boolean | number>} map
8
+ * @returns {string} string of classes joined together
9
+ */
10
+ const classMap = (map) => {
11
+ const classes = [];
12
+ for (const [key, value] of Object.entries(map)) {
13
+ if (value) classes.push(key);
14
+ }
15
+ return classes.join(' ');
16
+ };
17
+
18
+ /**
19
+ * Maps a list of styles to an element from an object.
20
+ * The keys will become the properties and the values will become the values.
21
+ * @param {Object.<string, string | undefined | null>} map
22
+ * @returns {string} string of styles joined together
23
+ */
24
+ const styleMap = (map) => {
25
+ const styles = [];
26
+ for (const [key, value] of Object.entries(map)) {
27
+ if (value) styles.push(`${key}:${value};`);
28
+ }
29
+ return styles.join(' ');
30
+ };
31
+
32
+ /**
33
+ * Renders one of two template parts based on a condition.
34
+ * @param {boolean} condition
35
+ * @param {TemplateLiteral | string} trueCase
36
+ * @param {TemplateLiteral | string} [falseCase]
37
+ * @returns {TemplateLiteral | string}
38
+ */
39
+ const when = (condition, trueCase, falseCase) => {
40
+ return condition ? trueCase : falseCase;
41
+ };
42
+
43
+ /**
44
+ * Chooses and evaluates a template part from a map of cases based on matching the given value to a case.
45
+ * @param {string} value
46
+ * @param {Object.<string, TemplateLiteral | string>} cases
47
+ * @param {TemplateLiteral | string} [defaultCase]
48
+ * @returns {TemplateLiteral | string}
49
+ */
50
+ const choose = (value, cases, defaultCase) => {
51
+ return cases[value] || defaultCase;
52
+ };
53
+
54
+ /**
55
+ * Renders a given string as HTML instead of text
56
+ * @param {string} string
57
+ * @returns {function(): string}
58
+ */
59
+ const unsafeHTML = (string) => {
60
+ return () => `${decodeAttribute(string)}`;
61
+ };
62
+
63
+ /**
64
+ * Renders multiple attributes as key="value" pairs from a map of attributes
65
+ * @param {Object.<string, any>} attributes
66
+ * @returns {function(): string}
67
+ */
68
+ const spreadAttributes = (attributes) => {
69
+ return () => {
70
+ return Object.keys(attributes)
71
+ .map((key) => {
72
+ return `${camelToDash(key)}='${new Part(attributes[key])}'`;
73
+ })
74
+ .join(' ');
75
+ };
76
+ };
77
+
78
+ export { classMap, styleMap, when, choose, unsafeHTML, spreadAttributes };
@@ -1,4 +1,5 @@
1
- import { camelToDash, decodeAttribute, encodeAttribute } from '../../../util/AttributeParser';
1
+ import { encodeAttribute } from '../../../util/AttributeParser';
2
+ import { unsafeHTML, spreadAttributes } from './directives.js';
2
3
 
3
4
  class Part {
4
5
  constructor(value) {
@@ -61,18 +62,4 @@ const html = function (strings, ...values) {
61
62
  return new TemplateLiteral(strings, ...values);
62
63
  };
63
64
 
64
- const unsafeHTML = (string) => {
65
- return () => `<!--$$plainly-set-inner-html-->${decodeAttribute(string)}`;
66
- };
67
-
68
- const spreadAttributes = (attributes) => {
69
- return () => {
70
- return Object.keys(attributes)
71
- .map((key) => {
72
- return `${camelToDash(key)}='${new Part(attributes[key])}'`;
73
- })
74
- .join(' ');
75
- };
76
- };
77
-
78
- export { html, unsafeHTML, spreadAttributes };
65
+ export { Part, TemplatePart, TemplateLiteral, html, unsafeHTML, spreadAttributes };
@@ -1,166 +1,50 @@
1
- const NODE_TYPE_COMMENT = 8;
2
- const NODE_TYPE_TEXT = 3;
3
-
4
1
  const _cachedTemplateElements = {};
5
2
 
6
- const convertStringToHTML = (templateString) => {
7
- const parser = new DOMParser();
8
- const document = parser.parseFromString(`<main>${templateString}</main>`, 'text/html');
9
-
10
- return document.querySelector('main');
11
- };
12
-
13
3
  /**
14
- * Create an array of the attributes on an element
15
- * @param {NamedNodeMap} attributes The attributes on an element
16
- * @return {Array} The attributes on an element as an array of key/value pairs
4
+ * Parses the given template string and returns a real DOM element.
5
+ * @param {string} template
6
+ * @returns {HTMLElement}
17
7
  */
18
- const getAttributes = function (attributes) {
19
- return Array.prototype.map.call(attributes, function (attribute) {
20
- return {
21
- attributeName: attribute.name,
22
- value: attribute.value,
23
- };
24
- });
8
+ const convertStringToHTML = (template) => {
9
+ const parser = new DOMParser();
10
+ const document = parser.parseFromString(template.toString(), 'text/html');
11
+ return document.body;
25
12
  };
26
13
 
27
14
  /**
28
- * Create a DOM Tree Map for an element
29
- *
30
- * @param {HTMLElement} element The element to map
31
- * @param {Boolean} isSVG If true, node is within an SVG
32
- * @return {{ domMap: Array, plainlySetInnerHTML?: boolean, innerHTML?: string }} A DOM tree map
15
+ * Diff the attributes for a live DOM element against a template DOM element
16
+ * @param {Element} templateElement The new template
17
+ * @param {Element} domElement The existing DOM node
33
18
  */
34
- const createDOMMap = function (element, isChild = false, isSVG = false, type = 'template') {
35
- const children = [...element.childNodes];
36
-
37
- // If the list contains a comment which says "$$plainly-set-inner-html" it means
38
- // that the all children of the element will be set as unsafe `innerHTML´
39
- // which means that there won't be any diffing or fancy things
40
- // but we will just mark it as a pure component and leave it be.
41
- for (const child of children) {
42
- if (child?.nodeType === NODE_TYPE_COMMENT && child.textContent === '$$plainly-set-inner-html') {
43
- return {
44
- domMap: [],
45
- plainlySetInnerHTML: true,
46
- innerHTML: element.innerHTML,
47
- };
19
+ const diffAttributes = function (templateElement, domElement) {
20
+ // remove or update previous attributes
21
+ for (/** @type {Attr} */ const attribute of Array.from(domElement.attributes)) {
22
+ if (templateElement.hasAttribute(attribute.name)) {
23
+ const oldValue = attribute.value;
24
+ const newValue = templateElement.getAttribute(attribute.name) || '';
25
+ if (oldValue !== newValue) {
26
+ domElement.setAttribute(attribute.name, newValue);
27
+ }
28
+ } else {
29
+ domElement.removeAttribute(attribute.name);
48
30
  }
49
31
  }
50
32
 
51
- const domMap = children.map((node, index) => {
52
- const details = {
53
- content: node.childNodes && node.childNodes.length > 0 ? null : node.textContent,
54
- attributes: node.nodeType !== 1 ? [] : getAttributes(node.attributes),
55
- type:
56
- node.nodeType === NODE_TYPE_TEXT
57
- ? 'text'
58
- : node.nodeType === NODE_TYPE_COMMENT
59
- ? 'comment'
60
- : node.tagName.toLowerCase(),
61
- node: node,
62
- };
63
-
64
- details.isSVG = isSVG || details.type === 'svg';
65
- details.children =
66
- isChild && isTemplateElement({ node }) ? { domMap: [] } : createDOMMap(node, true, details.isSVG);
67
- return details;
68
- });
69
-
70
- return {
71
- domMap,
72
- };
73
- };
74
-
75
- const removeAttributes = function (element, attributes) {
76
- attributes.forEach(function (attribute) {
77
- element.removeAttribute(attribute.attributeName);
78
- });
79
- };
80
-
81
- /**
82
- * Add attributes to an element
83
- * @param {HTMLElement} element The element
84
- * @param {[{ attributeName: string, value: string}]} attributes The attributes to add
85
- */
86
- const addAttributes = function (element, attributes) {
87
- attributes.forEach(function (attribute) {
88
- if (attribute.attributeName.startsWith('.')) {
89
- // Directly set the attribute on the element instance as property
90
- // instead of setting the attribute.
91
- element[attribute.attributeName.substring(1)] = attribute.value || '';
92
- return;
33
+ // set newly added attributes
34
+ for (/** @type {Attr} */ const attribute of Array.from(templateElement.attributes)) {
35
+ if (!domElement.hasAttribute(attribute.name)) {
36
+ domElement.setAttribute(attribute.name, attribute.value);
93
37
  }
94
-
95
- element.setAttribute(attribute.attributeName, attribute.value || '');
96
- });
97
- };
98
-
99
- /**
100
- * Diff the attributes on an existing element versus the template
101
- * @param {Object} template The new template
102
- * @param {Object} existing The existing DOM node
103
- */
104
- const diffAttributes = function (template, existing) {
105
- // Get attributes to remove
106
- const remove = existing.attributes.filter(function (attribute) {
107
- const getAtt = template.attributes.find(function (newAtt) {
108
- return attribute.attributeName === newAtt.attributeName;
109
- });
110
- return getAtt === undefined;
111
- });
112
-
113
- // Get attributes to change
114
- const change = template.attributes.filter(function (attribute) {
115
- const getAtt = existing.attributes.find((existingAtt) => {
116
- return attribute.attributeName === existingAtt.attributeName;
117
- });
118
- return getAtt === undefined || getAtt.value !== attribute.value;
119
- });
120
-
121
- // Add/remove any required attributes
122
- addAttributes(existing.node, change);
123
- removeAttributes(existing.node, remove);
38
+ }
124
39
  };
125
40
 
126
41
  /**
127
- * Make an HTML element
128
- * @param {Object} element The element details
129
- * @param {Object} templateResult
130
- * @return {HTMLElement|false} The HTML element
42
+ * Checks whether the given element or node is a TemplateElement or not
43
+ * @param {Element|Node} element
44
+ * @returns {boolean}
131
45
  */
132
- const makeElement = function (element, templateResult) {
133
- // Create the element
134
- let node;
135
- if (element.type === 'text') {
136
- node = document.createTextNode(element.content);
137
- } else if (element.type === 'comment') {
138
- node = document.createComment(element.content);
139
- } else if (element.isSVG) {
140
- node = document.createElementNS('http://www.w3.org/2000/svg', element.type);
141
- } else {
142
- node = document.createElement(element.type);
143
- }
144
-
145
- // Add attributes
146
- addAttributes(node, element.attributes);
147
-
148
- // If the element has child nodes, create them
149
- // Otherwise, add textContent
150
-
151
- if (element.children.domMap.length > 0) {
152
- diff(element.children, { domMap: [] }, node);
153
- } else if (templateResult.plainlySetInnerHTML) {
154
- node.innerHTML = templateResult.innerHTML;
155
- } else if (element.type !== 'text') {
156
- node.textContent = element.content;
157
- }
158
-
159
- return node;
160
- };
161
-
162
46
  const isTemplateElement = (element) => {
163
- const tagName = element.node?.tagName?.toLowerCase() ?? false;
47
+ const tagName = element.tagName?.toLowerCase() ?? false;
164
48
  if (!tagName || !tagName.includes('-')) {
165
49
  return false;
166
50
  }
@@ -180,99 +64,95 @@ const isTemplateElement = (element) => {
180
64
  };
181
65
 
182
66
  /**
183
- * Diff the existing DOM node versus the template
184
- * @param {{ domMap: Array, plainlySetInnerHTML?: boolean, innerHTML?: string }} templateResult A DOM tree map of the template content
185
- * @param {{ domMap: array }} domResult A DOM tree map of the existing DOM node
186
- * @param {HTMLElement} element The element to render content into
67
+ * Diff a live DOM node against a template DOM node
68
+ * @param {Node} templateNode
69
+ * @param {Node} domNode
187
70
  */
188
- const diff = function (templateResult, domResult, element) {
189
- const templateMap = templateResult.domMap;
190
-
191
- if (templateResult.plainlySetInnerHTML) {
192
- element.innerHTML = templateResult.innerHTML;
193
- return;
194
- }
195
-
196
- const domMap = domResult.domMap;
71
+ const diffNodes = function (templateNode, domNode) {
72
+ const templateChildNodes = [...templateNode.childNodes];
73
+ const domChildNodes = [...domNode.childNodes];
74
+
75
+ // Diff each node in the child node lists
76
+ const length = Math.max(templateChildNodes.length, domChildNodes.length);
77
+ for (let index = 0; index < length; index++) {
78
+ const templateChildNode = templateChildNodes[index];
79
+ const domChildNode = domChildNodes[index];
80
+
81
+ // If the DOM node doesn't exist, append/copy the template node
82
+ if (!domChildNode) {
83
+ domNode.appendChild(templateChildNode);
84
+ continue;
85
+ }
197
86
 
198
- // If extra elements in domMap, remove them
199
- let count = domMap.length - templateMap.length;
200
- if (count > 0) {
201
- for (; count > 0; count--) {
202
- domMap[domMap.length - count].node.parentNode.removeChild(domMap[domMap.length - count].node);
87
+ // If the template node doesn't exist, remove the node in the DOM
88
+ if (!templateChildNode) {
89
+ domChildNode.parentNode.removeChild(domChildNode);
90
+ continue;
203
91
  }
204
- }
205
92
 
206
- // Diff each item in the templateMap
207
- templateMap.forEach(function (node, index) {
208
- // If element doesn't exist, create it
209
- if (!domMap[index]) {
210
- const newElement = makeElement(node, node.children);
211
- element.appendChild(newElement);
212
- return;
93
+ // If DOM node is equal to the template node, don't do anything
94
+ if (domChildNode.isEqualNode(templateChildNode)) {
95
+ continue;
213
96
  }
214
97
 
215
- // If element is not the same type, replace it with new element
216
- if (templateMap[index].type !== domMap[index].type) {
217
- domMap[index].node.parentNode.replaceChild(
218
- makeElement(templateMap[index], node.children),
219
- domMap[index].node,
220
- );
221
- return;
98
+ // If node types are not the same, replace the DOM node with the template node
99
+ if (templateChildNode.nodeType !== domChildNode.nodeType) {
100
+ domChildNode.parentNode.replaceChild(templateChildNode, domChildNode);
101
+ continue;
222
102
  }
223
103
 
224
- // If attributes are different, update them
225
- diffAttributes(templateMap[index], domMap[index]);
104
+ // If the element tag names are not the same, replace the DOM node with the template node
105
+ if (templateChildNode.nodeType === 1 && templateChildNode.tagName !== domChildNode.tagName) {
106
+ domChildNode.parentNode.replaceChild(templateChildNode, domChildNode);
107
+ continue;
108
+ }
226
109
 
227
- if (node.children.plainlySetInnerHTML && domMap[index]) {
228
- domMap[index].innerHTML = node.children.innerHTML;
229
- return;
110
+ // If the node is an SVG element, don't even think about diffing it, just replace it
111
+ if (templateChildNode.nodeType === 1 && templateChildNode.tagName === 'SVG') {
112
+ domChildNode.parentNode.replaceChild(templateChildNode, domChildNode);
113
+ continue;
230
114
  }
231
115
 
232
- // If content is different, update it
233
- if (templateMap[index].content !== domMap[index].content) {
234
- if (!isTemplateElement(domMap[index])) {
235
- // ignore custom elements.
236
- domMap[index].node.textContent = templateMap[index].content;
237
- }
116
+ // If the node is an Element node, diff the attributes
117
+ if (templateChildNode.nodeType === 1) {
118
+ diffAttributes(templateChildNode, domChildNode);
238
119
  }
239
120
 
240
- // If target element should be empty, wipe it
241
- if (domMap[index].children.domMap.length > 0 && node.children.domMap.length < 1) {
242
- if (!isTemplateElement(domMap[index])) {
243
- // ignore custom elements.
244
- domMap[index].node.innerHTML = '';
245
- }
246
- return;
121
+ // If the node is a Text node or a Comment node, update it
122
+ if (domChildNode.nodeType === 3 || domChildNode.nodeType === 8) {
123
+ domChildNode.textContent = templateChildNode.textContent;
124
+ continue;
247
125
  }
248
126
 
249
- // If element is empty and shouldn't be, build it up
250
- // This uses a document fragment to minimize reflows
251
- if (domMap[index].children.domMap.length === 0 && node.children.domMap.length > 0) {
252
- if (isTemplateElement(domMap[index])) {
253
- return;
254
- }
127
+ // If the DOM node should be empty, remove all child nodes
128
+ if (domChildNode.hasChildNodes() && !templateChildNode.hasChildNodes()) {
129
+ domChildNode.replaceChildren();
130
+ continue;
131
+ }
255
132
 
256
- const fragment = document.createDocumentFragment();
257
- diff(node.children, domMap[index].children, domMap[index].node);
258
- element.appendChild(fragment);
259
- return;
133
+ // If the DOM node is empty, but should have children, add child nodes from the template node
134
+ if (!domChildNode.hasChildNodes() && templateChildNode.hasChildNodes()) {
135
+ domChildNode.replaceChildren(...templateChildNode.childNodes);
136
+ continue;
260
137
  }
261
138
 
262
- // If there are existing child elements that need to be modified, diff them
263
- if (node.children.domMap.length > 0) {
264
- if (!isTemplateElement(node)) {
265
- diff(node.children, domMap[index].children, domMap[index].node);
139
+ // If there are child nodes, diff them recursively
140
+ if (templateChildNode.hasChildNodes()) {
141
+ if (!isTemplateElement(templateChildNode)) {
142
+ diffNodes(templateChildNode, domChildNode);
266
143
  }
267
144
  }
268
- });
145
+ }
269
146
  };
270
147
 
271
- const render = (template, target) => {
272
- const templateDOMMap = createDOMMap(convertStringToHTML(template), false, false, 'template');
273
- const domMap = createDOMMap(target, false, false, 'dom');
274
-
275
- diff(templateDOMMap, domMap, target);
148
+ /**
149
+ * Render a template string into the given DOM node
150
+ * @param {string} template
151
+ * @param {Node} domNode
152
+ */
153
+ const render = (template, domNode) => {
154
+ const templateNode = convertStringToHTML(template);
155
+ diffNodes(templateNode, domNode);
276
156
  };
277
157
 
278
158
  export { render };
@@ -71,20 +71,38 @@ export function camelToDash(string) {
71
71
  return string.replace(/([A-Z])/g, '-$1').toLowerCase();
72
72
  }
73
73
 
74
+ /**
75
+ * Decodes an attribute. Can only be encoded with the `encodeAttribute` method which
76
+ * is exported from this file.
77
+ *
78
+ * @param attribute
79
+ * @returns {string}
80
+ */
74
81
  export function decodeAttribute(attribute) {
75
82
  return `${attribute}`
76
- .replace(/&lt;/g, '<')
77
- .replace(/&gt;/g, '>')
78
- .replace(/&quot;/g, '"')
79
- .replace(/&apos;/g, "'");
83
+ .replace(/\$lt;/g, '<')
84
+ .replace(/\$gt;/g, '>')
85
+ .replace(/\$quot;/g, '"')
86
+ .replace(/\$apos;/g, "'");
80
87
  }
81
88
 
89
+ /**
90
+ * Encodes an attribute. Can only be decoded with the `decodeAttribute` method which
91
+ * is exported from this file.
92
+ *
93
+ * This does not use the standard way to encode an attribute, as the dom parser
94
+ * that renders the html would understand and automatically decode it. Which results
95
+ * in non sanitized html output.
96
+ *
97
+ * @param attribute
98
+ * @returns {string}
99
+ */
82
100
  export function encodeAttribute(attribute) {
83
101
  return `${attribute}`
84
- .replace(/'/g, '&apos;')
85
- .replace(/"/g, '&quot;')
86
- .replace(/</g, '&lt;')
87
- .replace(/>/g, '&gt;')
102
+ .replace(/'/g, '$apos;')
103
+ .replace(/"/g, '$quot;')
104
+ .replace(/</g, '$lt;')
105
+ .replace(/>/g, '$gt;')
88
106
  .replace(/\r\n/g, '\n')
89
107
  .replace(/[\r\n]/g, '\n');
90
108
  }
@@ -0,0 +1,57 @@
1
+ import { isObjectLike } from './AttributeParser';
2
+
3
+ export class Store {
4
+ _observer = new Set();
5
+ _singlePropertyMode = false;
6
+ _state = {};
7
+
8
+ constructor(value) {
9
+ // wrap primitives with a generic "value" field to generate getter and setter
10
+ this._singlePropertyMode = !isObjectLike(value);
11
+ const specificValues = this._singlePropertyMode ? { value } : value;
12
+ this._state = { ...(!this._singlePropertyMode && this.properties()), ...specificValues };
13
+
14
+ Object.entries(this._state).map(([key, value]) => {
15
+ Object.defineProperty(this, key, {
16
+ get: () => {
17
+ return this._state[key];
18
+ },
19
+ set: (newValue) => {
20
+ this._state[key] = newValue;
21
+ this.requestUpdate();
22
+ },
23
+ });
24
+ });
25
+ }
26
+
27
+ properties() {
28
+ return {};
29
+ }
30
+
31
+ valueOf() {
32
+ return this._singlePropertyMode ? this._state.value : this._state;
33
+ }
34
+
35
+ toString() {
36
+ return `${this._singlePropertyMode ? this._state.value : this._state}`;
37
+ }
38
+
39
+ /**
40
+ * @param {BaseElement|Function} observer
41
+ */
42
+ subscribe(observer) {
43
+ this._observer.add(observer);
44
+ }
45
+ /**
46
+ * @param {BaseElement|Function} observer
47
+ */
48
+ unsubscribe(observer) {
49
+ this._observer.delete(observer);
50
+ }
51
+
52
+ requestUpdate() {
53
+ this._observer.forEach((observer) =>
54
+ typeof observer.requestUpdate === 'function' ? observer.requestUpdate() : observer(),
55
+ );
56
+ }
57
+ }