@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/CHANGELOG.md +14 -0
- package/README.md +6 -3
- package/dist/index.js +50 -0
- package/dist/index.js.map +3 -3
- package/index.js +2 -1
- package/package.json +8 -3
- package/src/BaseElement.js +12 -0
- package/src/renderer/vanilla/util/directives.js +78 -0
- package/src/renderer/vanilla/util/html.js +3 -16
- package/src/renderer/vanilla/util/render.js +97 -217
- package/src/util/AttributeParser.js +26 -8
- package/src/util/Store.js +57 -0
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.
|
|
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
|
}
|
package/src/BaseElement.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
*
|
|
15
|
-
* @param
|
|
16
|
-
* @
|
|
4
|
+
* Parses the given template string and returns a real DOM element.
|
|
5
|
+
* @param {string} template
|
|
6
|
+
* @returns {HTMLElement}
|
|
17
7
|
*/
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @param {
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
*
|
|
128
|
-
* @param
|
|
129
|
-
* @
|
|
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.
|
|
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
|
|
184
|
-
* @param
|
|
185
|
-
* @param
|
|
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
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
233
|
-
if (
|
|
234
|
-
|
|
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
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
263
|
-
if (
|
|
264
|
-
if (!isTemplateElement(
|
|
265
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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(
|
|
77
|
-
.replace(
|
|
78
|
-
.replace(
|
|
79
|
-
.replace(
|
|
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, '
|
|
85
|
-
.replace(/"/g, '
|
|
86
|
-
.replace(/</g, '
|
|
87
|
-
.replace(/>/g, '
|
|
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
|
+
}
|