├── .gitignore
├── 01 - state
├── 01 - manual dom manipulation.html
└── 02 - state-based ui.html
├── 02 - dom diffing
├── 01 - what is dom diffing.html
├── 02 - converting a string to HTML.html
├── 03 - loop through each element.html
├── 04 - a rendering function.html
└── render.js
├── 03 - reactivity
├── 01 - what is reactivity.html
├── 02 - setter functions.html
├── 03 - proxies.html
├── 04 - nested arrays and objects in proxies.html
├── 05 - creating proxies from proxies.html
├── 06 - detecting proxies.html
├── 07 - proxies and reactivity.html
├── render.js
└── store.js
├── 04 - components
├── 01 - reactive component.html
├── 02 - node selectors.html
├── 03 - component methods
│ ├── component.js
│ └── index.html
├── 04 - lifecycle events
│ ├── component.js
│ └── index.html
├── render.js
└── store.js
├── 05 - performance
├── 01 - batching renders.html
├── 02 - memoization and ids.html
├── 03 - virtual dom.html
├── 04 - why use a virtual dom.html
├── component.js
├── render.js
└── store.js
├── 06 - project
├── 01 - project template.html
├── 02 - project completed.html
├── component.js
├── dogs.js
├── img
│ ├── bandit.jpg
│ ├── bert.jpg
│ ├── brad.jpg
│ ├── chewie.jpg
│ ├── daphne.jpg
│ ├── ethel.jpg
│ ├── florence.jpg
│ ├── grace.jpg
│ ├── janice.jpg
│ ├── joseph.jpg
│ ├── kyle.jpg
│ ├── reba.jpg
│ ├── thor.jpg
│ └── william.jpg
├── render.js
└── store.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node
2 | node_modules
3 | test/results
4 | test/coverage
5 |
6 | ## OS X
7 | .DS_Store
8 | ._*
9 | .Spotlight-V100
10 | .Trashes
--------------------------------------------------------------------------------
/01 - state/01 - manual dom manipulation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Manual DOM Manipulation
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 | Manual DOM Manipulation
21 |
22 |
23 | - Gandalf
24 | - Radagast
25 | - Merlin
26 |
27 |
28 |
29 |
50 |
51 |
--------------------------------------------------------------------------------
/01 - state/02 - state-based ui.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | State-Based UI
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 | State-Based UI
21 |
22 |
23 |
24 |
25 |
67 |
68 |
--------------------------------------------------------------------------------
/02 - dom diffing/01 - what is dom diffing.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | What is DOM diffing?
7 |
8 |
9 |
20 |
21 |
22 |
23 |
24 |
25 | Wizards
26 |
27 | - Gandalf
28 | - Radagast
29 | - Merlin
30 |
31 |
32 |
33 | Magical Folk
34 |
35 | - Gandalf
36 | - Radagast
37 | - Merlin
38 | - Ursula
39 |
40 |
41 |
51 |
52 |
--------------------------------------------------------------------------------
/02 - dom diffing/02 - converting a string to HTML.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Converting a string to HTML
7 |
8 |
9 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
77 |
78 |
--------------------------------------------------------------------------------
/02 - dom diffing/03 - loop through each element.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Loop through each element
7 |
8 |
9 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
122 |
123 |
--------------------------------------------------------------------------------
/02 - dom diffing/04 - a rendering function.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | A rendering function
7 |
8 |
9 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
75 |
76 |
--------------------------------------------------------------------------------
/02 - dom diffing/render.js:
--------------------------------------------------------------------------------
1 | let render = (function () {
2 |
3 | // Form fields and attributes that can be modified by users
4 | // They also have implicit values that make it hard to know if they were changed by the user or developer
5 | let formFields = ['input', 'option', 'textarea'];
6 | let formAtts = ['value', 'checked', 'selected'];
7 | let formAttsNoVal = ['checked', 'selected'];
8 |
9 | /**
10 | * Convert a template string into HTML DOM nodes
11 | * @param {String} str The template string
12 | * @return {Node} The template HTML
13 | */
14 | function stringToHTML (str) {
15 |
16 | // Create document
17 | let parser = new DOMParser();
18 | let doc = parser.parseFromString(str, 'text/html');
19 |
20 | // If there are items in the head, move them to the body
21 | if (doc.head && doc.head.childNodes.length) {
22 | Array.from(doc.head.childNodes).reverse().forEach(function (node) {
23 | doc.body.insertBefore(node, doc.body.firstChild);
24 | });
25 | }
26 |
27 | return doc.body || document.createElement('body');
28 |
29 | }
30 |
31 | /**
32 | * Check if an attribute string has a stringified falsy value
33 | * @param {String} str The string
34 | * @return {Boolean} If true, value is falsy (yea, I know, that's a little confusing)
35 | */
36 | function isFalsy (str) {
37 | return ['false', 'null', 'undefined', '0', '-0', 'NaN', '0n', '-0n'].includes(str);
38 | }
39 |
40 | /**
41 | * Check if attribute should be skipped (sanitize properties)
42 | * @param {String} name The attribute name
43 | * @param {String} value The attribute value
44 | * @return {Boolean} If true, skip the attribute
45 | */
46 | function skipAttribute (name, value) {
47 | let val = value.replace(/\s+/g, '').toLowerCase();
48 | if (['src', 'href', 'xlink:href'].includes(name)) {
49 | if (val.includes('javascript:') || val.includes('data:text/html')) return true;
50 | }
51 | if (name.startsWith('on')) return true;
52 | }
53 |
54 | /**
55 | * Add an attribute to an element
56 | * @param {Node} elem The element
57 | * @param {String} att The attribute
58 | * @param {String} val The value
59 | */
60 | function addAttribute (elem, att, val) {
61 |
62 | // Sanitize dangerous attributes
63 | if (skipAttribute(att, val)) return;
64 |
65 | // If it's a form attribute, set the property directly
66 | if (formAtts.includes(att)) {
67 | elem[att] = att === 'value' ? val : ' ';
68 | }
69 |
70 | // Update the attribute
71 | elem.setAttribute(att, val);
72 |
73 |
74 | }
75 |
76 | /**
77 | * Remove an attribute from an element
78 | * @param {Node} elem The element
79 | * @param {String} att The attribute
80 | */
81 | function removeAttribute (elem, att) {
82 |
83 | // If it's a form attribute, remove the property directly
84 | if (formAtts.includes(att)) {
85 | elem[att] = '';
86 | }
87 |
88 | // Remove the attribute
89 | elem.removeAttribute(att);
90 |
91 | }
92 |
93 | /**
94 | * Compare the existing node attributes to the template node attributes and make updates
95 | * @param {Node} template The new template
96 | * @param {Node} existing The existing DOM node
97 | */
98 | function diffAttributes (template, existing) {
99 |
100 | // If the node is not an element, bail
101 | if (template.nodeType !== 1) return;
102 |
103 | // Get attributes for the template and existing DOM
104 | let templateAtts = template.attributes;
105 | let existingAtts = existing.attributes;
106 |
107 | // Add and update attributes from the template into the DOM
108 | for (let {name, value} of templateAtts) {
109 |
110 | // Skip user-editable form field attributes
111 | if (formAtts.includes(name) && formFields.includes(template.tagName.toLowerCase())) continue;
112 |
113 | // If its a no-value property and it's falsy remove it
114 | if (formAttsNoVal.includes(name) && isFalsy(value)) {
115 | removeAttribute(existing, name);
116 | continue;
117 | }
118 |
119 | // Otherwise, add the attribute
120 | addAttribute(existing, name, value);
121 |
122 | }
123 |
124 | // Remove attributes from the DOM that shouldn't be there
125 | for (let {name, value} of existingAtts) {
126 |
127 | // If the attribute exists in the template, skip it
128 | if (templateAtts[name]) continue;
129 |
130 | // Skip user-editable form field attributes
131 | if (formAtts.includes(name) && formFields.includes(existing.tagName.toLowerCase())) continue;
132 |
133 | // Otherwise, remove it
134 | removeAttribute(existing, name);
135 |
136 | }
137 |
138 | }
139 |
140 | /**
141 | * Add default attributes to a newly created element
142 | * @param {Node} elem The element
143 | */
144 | function addDefaultAtts (elem) {
145 |
146 | // Only run on elements
147 | if (elem.nodeType !== 1) return;
148 |
149 | // Remove unsafe HTML attributes
150 | for (let {name, value} of elem.attributes) {
151 |
152 | // If the attribute should be skipped, remove it
153 | if (skipAttribute(name, value)) {
154 | removeAttribute(elem, name);
155 | continue;
156 | }
157 |
158 | // If it's a no-value attribute and its falsy, skip it
159 | if (formAttsNoVal.includes(name) && isFalsy(value)) continue;
160 |
161 | // Add the plain attribute
162 | addAttribute(elem, name, value);
163 |
164 | }
165 |
166 | // If there are child elems, recursively add defaults to them
167 | if (elem.childNodes) {
168 | for (let node of elem.childNodes) {
169 | addDefaultAtts(node);
170 | }
171 | }
172 |
173 | }
174 |
175 | /**
176 | * Get the content from a node
177 | * @param {Node} node The node
178 | * @return {String} The content
179 | */
180 | function getNodeContent (node) {
181 | return node.childNodes && node.childNodes.length ? null : node.textContent;
182 | }
183 |
184 | /**
185 | * Check if two nodes are different
186 | * @param {Node} node1 The first node
187 | * @param {Node} node2 The second node
188 | * @return {Boolean} If true, they're not the same node
189 | */
190 | function isDifferentNode (node1, node2) {
191 | return (
192 | (typeof node1.nodeType === 'number' && node1.nodeType !== node2.nodeType) ||
193 | (typeof node1.tagName === 'string' && node1.tagName !== node2.tagName) ||
194 | (typeof node1.id === 'string' && node1.id !== node2.id) ||
195 | (typeof node1.src === 'string' && node1.src !== node2.src)
196 | );
197 | }
198 |
199 | /**
200 | * Check if the desired node is further ahead in the DOM existingNodes
201 | * @param {Node} node The node to look for
202 | * @param {NodeList} existingNodes The DOM existingNodes
203 | * @param {Integer} index The indexing index
204 | * @return {Integer} How many nodes ahead the target node is
205 | */
206 | function aheadInTree (node, existingNodes, index) {
207 | return Array.from(existingNodes).slice(index + 1).find(function (branch) {
208 | return !isDifferentNode(node, branch);
209 | });
210 | }
211 |
212 | /**
213 | * If there are extra elements in DOM, remove them
214 | * @param {Array} existingNodes The existing DOM
215 | * @param {Array} templateNodes The template
216 | */
217 | function trimExtraNodes (existingNodes, templateNodes) {
218 | let extra = existingNodes.length - templateNodes.length;
219 | if (extra < 1) return;
220 | for (; extra > 0; extra--) {
221 | existingNodes[existingNodes.length - 1].remove();
222 | }
223 | }
224 |
225 | /**
226 | * Remove scripts from HTML
227 | * @param {Node} elem The element to remove scripts from
228 | */
229 | function removeScripts (elem) {
230 | let scripts = elem.querySelectorAll('script');
231 | for (let script of scripts) {
232 | script.remove();
233 | }
234 | }
235 |
236 | /**
237 | * Diff the existing DOM node versus the template
238 | * @param {Array} template The template HTML
239 | * @param {Node} existing The current DOM HTML
240 | */
241 | function diff (template, existing) {
242 |
243 | // Get the nodes in the template and existing UI
244 | let templateNodes = template.childNodes;
245 | let existingNodes = existing.childNodes;
246 |
247 | // Don't inject scripts
248 | if (removeScripts(template)) return;
249 |
250 | // Loop through each node in the template and compare it to the matching element in the UI
251 | templateNodes.forEach(function (node, index) {
252 |
253 | // If element doesn't exist, create it
254 | if (!existingNodes[index]) {
255 | let clone = node.cloneNode(true);
256 | addDefaultAtts(clone);
257 | existing.append(clone);
258 | return;
259 | }
260 |
261 | // If there is, but it's not the same node type, insert the new node before the existing one
262 | if (isDifferentNode(node, existingNodes[index])) {
263 |
264 | // Check if node exists further in the tree
265 | let ahead = aheadInTree(node, existingNodes, index);
266 |
267 | // If not, insert the node before the current one
268 | if (!ahead) {
269 | let clone = node.cloneNode(true);
270 | addDefaultAtts(clone);
271 | existingNodes[index].before(clone);
272 | return;
273 | }
274 |
275 | // Otherwise, move it to the current spot
276 | existingNodes[index].before(ahead);
277 |
278 | }
279 |
280 | // If attributes are different, update them
281 | diffAttributes(node, existingNodes[index]);
282 |
283 | // Stop diffing if a native web component
284 | if (node.nodeName.includes('-')) return;
285 |
286 | // If content is different, update it
287 | let templateContent = getNodeContent(node);
288 | if (templateContent && templateContent !== getNodeContent(existingNodes[index])) {
289 | existingNodes[index].textContent = templateContent;
290 | }
291 |
292 | // If there shouldn't be child nodes but there are, remove them
293 | if (!node.childNodes.length && existingNodes[index].childNodes.length) {
294 | existingNodes[index].innerHTML = '';
295 | return;
296 | }
297 |
298 | // If DOM is empty and shouldn't be, build it up
299 | // This uses a document fragment to minimize reflows
300 | if (!existingNodes[index].childNodes.length && node.childNodes.length) {
301 | let fragment = document.createDocumentFragment();
302 | diff(node, fragment);
303 | existingNodes[index].appendChild(fragment);
304 | return;
305 | }
306 |
307 | // If there are nodes within it, recursively diff those
308 | if (node.childNodes.length) {
309 | diff(node, existingNodes[index]);
310 | }
311 |
312 | });
313 |
314 | // If extra elements in DOM, remove them
315 | trimExtraNodes(existingNodes, templateNodes);
316 |
317 | }
318 |
319 | /**
320 | * Render a template into the UI
321 | * @param {Node|String} elem The element or selector to render the template into
322 | * @param {String} template The template to render
323 | */
324 | function render (elem, template) {
325 | let node = elem === 'string' ? document.querySelector(elem) : elem;
326 | let html = stringToHTML(template);
327 | diff(html, node);
328 | }
329 |
330 | return render;
331 |
332 | })();
--------------------------------------------------------------------------------
/03 - reactivity/01 - what is reactivity.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | What is reactivity?
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 | What is reactivity?
21 |
22 |
23 |
24 |
25 |
26 |
58 |
59 |
--------------------------------------------------------------------------------
/03 - reactivity/02 - setter functions.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Setter Functions
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 | Setter Functions
21 |
22 |
23 |
24 |
25 |
26 |
79 |
80 |
--------------------------------------------------------------------------------
/03 - reactivity/03 - proxies.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Proxies
7 |
8 |
9 |
10 |
11 |
12 |
13 |
69 |
70 |
--------------------------------------------------------------------------------
/03 - reactivity/04 - nested arrays and objects in proxies.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Nested arrays and objects in proxies
7 |
8 |
9 |
10 |
11 |
12 |
73 |
74 |
--------------------------------------------------------------------------------
/03 - reactivity/05 - creating proxies from proxies.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Creating proxies from proxies
7 |
8 |
9 |
10 |
11 |
12 |
81 |
82 |
--------------------------------------------------------------------------------
/03 - reactivity/06 - detecting proxies.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Detecting proxies
7 |
8 |
9 |
10 |
11 |
12 |
84 |
85 |
--------------------------------------------------------------------------------
/03 - reactivity/07 - proxies and reactivity.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Proxies & Reactivity
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
74 |
75 |
--------------------------------------------------------------------------------
/03 - reactivity/render.js:
--------------------------------------------------------------------------------
1 | let render = (function () {
2 |
3 | // Form fields and attributes that can be modified by users
4 | // They also have implicit values that make it hard to know if they were changed by the user or developer
5 | let formFields = ['input', 'option', 'textarea'];
6 | let formAtts = ['value', 'checked', 'selected'];
7 | let formAttsNoVal = ['checked', 'selected'];
8 |
9 | /**
10 | * Convert a template string into HTML DOM nodes
11 | * @param {String} str The template string
12 | * @return {Node} The template HTML
13 | */
14 | function stringToHTML (str) {
15 |
16 | // Create document
17 | let parser = new DOMParser();
18 | let doc = parser.parseFromString(str, 'text/html');
19 |
20 | // If there are items in the head, move them to the body
21 | if (doc.head && doc.head.childNodes.length) {
22 | Array.from(doc.head.childNodes).reverse().forEach(function (node) {
23 | doc.body.insertBefore(node, doc.body.firstChild);
24 | });
25 | }
26 |
27 | return doc.body || document.createElement('body');
28 |
29 | }
30 |
31 | /**
32 | * Check if an attribute string has a stringified falsy value
33 | * @param {String} str The string
34 | * @return {Boolean} If true, value is falsy (yea, I know, that's a little confusing)
35 | */
36 | function isFalsy (str) {
37 | return ['false', 'null', 'undefined', '0', '-0', 'NaN', '0n', '-0n'].includes(str);
38 | }
39 |
40 | /**
41 | * Check if attribute should be skipped (sanitize properties)
42 | * @param {String} name The attribute name
43 | * @param {String} value The attribute value
44 | * @return {Boolean} If true, skip the attribute
45 | */
46 | function skipAttribute (name, value) {
47 | let val = value.replace(/\s+/g, '').toLowerCase();
48 | if (['src', 'href', 'xlink:href'].includes(name)) {
49 | if (val.includes('javascript:') || val.includes('data:text/html')) return true;
50 | }
51 | if (name.startsWith('on')) return true;
52 | }
53 |
54 | /**
55 | * Add an attribute to an element
56 | * @param {Node} elem The element
57 | * @param {String} att The attribute
58 | * @param {String} val The value
59 | */
60 | function addAttribute (elem, att, val) {
61 |
62 | // Sanitize dangerous attributes
63 | if (skipAttribute(att, val)) return;
64 |
65 | // If it's a form attribute, set the property directly
66 | if (formAtts.includes(att)) {
67 | elem[att] = att === 'value' ? val : ' ';
68 | }
69 |
70 | // Update the attribute
71 | elem.setAttribute(att, val);
72 |
73 |
74 | }
75 |
76 | /**
77 | * Remove an attribute from an element
78 | * @param {Node} elem The element
79 | * @param {String} att The attribute
80 | */
81 | function removeAttribute (elem, att) {
82 |
83 | // If it's a form attribute, remove the property directly
84 | if (formAtts.includes(att)) {
85 | elem[att] = '';
86 | }
87 |
88 | // Remove the attribute
89 | elem.removeAttribute(att);
90 |
91 | }
92 |
93 | /**
94 | * Compare the existing node attributes to the template node attributes and make updates
95 | * @param {Node} template The new template
96 | * @param {Node} existing The existing DOM node
97 | */
98 | function diffAttributes (template, existing) {
99 |
100 | // If the node is not an element, bail
101 | if (template.nodeType !== 1) return;
102 |
103 | // Get attributes for the template and existing DOM
104 | let templateAtts = template.attributes;
105 | let existingAtts = existing.attributes;
106 |
107 | // Add and update attributes from the template into the DOM
108 | for (let {name, value} of templateAtts) {
109 |
110 | // Skip user-editable form field attributes
111 | if (formAtts.includes(name) && formFields.includes(template.tagName.toLowerCase())) continue;
112 |
113 | // If its a no-value property and it's falsy remove it
114 | if (formAttsNoVal.includes(name) && isFalsy(value)) {
115 | removeAttribute(existing, name);
116 | continue;
117 | }
118 |
119 | // Otherwise, add the attribute
120 | addAttribute(existing, name, value);
121 |
122 | }
123 |
124 | // Remove attributes from the DOM that shouldn't be there
125 | for (let {name, value} of existingAtts) {
126 |
127 | // If the attribute exists in the template, skip it
128 | if (templateAtts[name]) continue;
129 |
130 | // Skip user-editable form field attributes
131 | if (formAtts.includes(name) && formFields.includes(existing.tagName.toLowerCase())) continue;
132 |
133 | // Otherwise, remove it
134 | removeAttribute(existing, name);
135 |
136 | }
137 |
138 | }
139 |
140 | /**
141 | * Add default attributes to a newly created element
142 | * @param {Node} elem The element
143 | */
144 | function addDefaultAtts (elem) {
145 |
146 | // Only run on elements
147 | if (elem.nodeType !== 1) return;
148 |
149 | // Remove unsafe HTML attributes
150 | for (let {name, value} of elem.attributes) {
151 |
152 | // If the attribute should be skipped, remove it
153 | if (skipAttribute(name, value)) {
154 | removeAttribute(elem, name);
155 | continue;
156 | }
157 |
158 | // If it's a no-value attribute and its falsy, skip it
159 | if (formAttsNoVal.includes(name) && isFalsy(value)) continue;
160 |
161 | // Add the plain attribute
162 | addAttribute(elem, name, value);
163 |
164 | }
165 |
166 | // If there are child elems, recursively add defaults to them
167 | if (elem.childNodes) {
168 | for (let node of elem.childNodes) {
169 | addDefaultAtts(node);
170 | }
171 | }
172 |
173 | }
174 |
175 | /**
176 | * Get the content from a node
177 | * @param {Node} node The node
178 | * @return {String} The content
179 | */
180 | function getNodeContent (node) {
181 | return node.childNodes && node.childNodes.length ? null : node.textContent;
182 | }
183 |
184 | /**
185 | * Check if two nodes are different
186 | * @param {Node} node1 The first node
187 | * @param {Node} node2 The second node
188 | * @return {Boolean} If true, they're not the same node
189 | */
190 | function isDifferentNode (node1, node2) {
191 | return (
192 | (typeof node1.nodeType === 'number' && node1.nodeType !== node2.nodeType) ||
193 | (typeof node1.tagName === 'string' && node1.tagName !== node2.tagName) ||
194 | (typeof node1.id === 'string' && node1.id !== node2.id) ||
195 | (typeof node1.src === 'string' && node1.src !== node2.src)
196 | );
197 | }
198 |
199 | /**
200 | * Check if the desired node is further ahead in the DOM existingNodes
201 | * @param {Node} node The node to look for
202 | * @param {NodeList} existingNodes The DOM existingNodes
203 | * @param {Integer} index The indexing index
204 | * @return {Integer} How many nodes ahead the target node is
205 | */
206 | function aheadInTree (node, existingNodes, index) {
207 | return Array.from(existingNodes).slice(index + 1).find(function (branch) {
208 | return !isDifferentNode(node, branch);
209 | });
210 | }
211 |
212 | /**
213 | * If there are extra elements in DOM, remove them
214 | * @param {Array} existingNodes The existing DOM
215 | * @param {Array} templateNodes The template
216 | */
217 | function trimExtraNodes (existingNodes, templateNodes) {
218 | let extra = existingNodes.length - templateNodes.length;
219 | if (extra < 1) return;
220 | for (; extra > 0; extra--) {
221 | existingNodes[existingNodes.length - 1].remove();
222 | }
223 | }
224 |
225 | /**
226 | * Remove scripts from HTML
227 | * @param {Node} elem The element to remove scripts from
228 | */
229 | function removeScripts (elem) {
230 | let scripts = elem.querySelectorAll('script');
231 | for (let script of scripts) {
232 | script.remove();
233 | }
234 | }
235 |
236 | /**
237 | * Diff the existing DOM node versus the template
238 | * @param {Array} template The template HTML
239 | * @param {Node} existing The current DOM HTML
240 | */
241 | function diff (template, existing) {
242 |
243 | // Get the nodes in the template and existing UI
244 | let templateNodes = template.childNodes;
245 | let existingNodes = existing.childNodes;
246 |
247 | // Don't inject scripts
248 | if (removeScripts(template)) return;
249 |
250 | // Loop through each node in the template and compare it to the matching element in the UI
251 | templateNodes.forEach(function (node, index) {
252 |
253 | // If element doesn't exist, create it
254 | if (!existingNodes[index]) {
255 | let clone = node.cloneNode(true);
256 | addDefaultAtts(clone);
257 | existing.append(clone);
258 | return;
259 | }
260 |
261 | // If there is, but it's not the same node type, insert the new node before the existing one
262 | if (isDifferentNode(node, existingNodes[index])) {
263 |
264 | // Check if node exists further in the tree
265 | let ahead = aheadInTree(node, existingNodes, index);
266 |
267 | // If not, insert the node before the current one
268 | if (!ahead) {
269 | let clone = node.cloneNode(true);
270 | addDefaultAtts(clone);
271 | existingNodes[index].before(clone);
272 | return;
273 | }
274 |
275 | // Otherwise, move it to the current spot
276 | existingNodes[index].before(ahead);
277 |
278 | }
279 |
280 | // If attributes are different, update them
281 | diffAttributes(node, existingNodes[index]);
282 |
283 | // Stop diffing if a native web component
284 | if (node.nodeName.includes('-')) return;
285 |
286 | // If content is different, update it
287 | let templateContent = getNodeContent(node);
288 | if (templateContent && templateContent !== getNodeContent(existingNodes[index])) {
289 | existingNodes[index].textContent = templateContent;
290 | }
291 |
292 | // If there shouldn't be child nodes but there are, remove them
293 | if (!node.childNodes.length && existingNodes[index].childNodes.length) {
294 | existingNodes[index].innerHTML = '';
295 | return;
296 | }
297 |
298 | // If DOM is empty and shouldn't be, build it up
299 | // This uses a document fragment to minimize reflows
300 | if (!existingNodes[index].childNodes.length && node.childNodes.length) {
301 | let fragment = document.createDocumentFragment();
302 | diff(node, fragment);
303 | existingNodes[index].appendChild(fragment);
304 | return;
305 | }
306 |
307 | // If there are nodes within it, recursively diff those
308 | if (node.childNodes.length) {
309 | diff(node, existingNodes[index]);
310 | }
311 |
312 | });
313 |
314 | // If extra elements in DOM, remove them
315 | trimExtraNodes(existingNodes, templateNodes);
316 |
317 | }
318 |
319 | /**
320 | * Render a template into the UI
321 | * @param {Node|String} elem The element or selector to render the template into
322 | * @param {String} template The template to render
323 | */
324 | function render (elem, template) {
325 | let node = elem === 'string' ? document.querySelector(elem) : elem;
326 | let html = stringToHTML(template);
327 | diff(html, node);
328 | }
329 |
330 | return render;
331 |
332 | })();
--------------------------------------------------------------------------------
/03 - reactivity/store.js:
--------------------------------------------------------------------------------
1 | let store = (function () {
2 |
3 | /**
4 | * Emit a custom event
5 | * @param {*} data The store data
6 | */
7 | function emit (data) {
8 |
9 | // Create a new event
10 | let event = new CustomEvent('store', {
11 | bubbles: true,
12 | cancelable: true,
13 | detail: data
14 | });
15 |
16 | // Dispatch the event
17 | return document.dispatchEvent(event);
18 |
19 | }
20 |
21 | /**
22 | * Create a Proxy handler object
23 | * @param {Object} data The data object
24 | * @return {Object} The handler object
25 | */
26 | function handler (data) {
27 | return {
28 | get (obj, prop) {
29 | if (prop === '_isProxy') return true;
30 | if (['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(obj[prop])) && !obj[prop]._isProxy) {
31 | obj[prop] = new Proxy(obj[prop], handler(data));
32 | }
33 | return obj[prop];
34 | },
35 | set (obj, prop, value) {
36 | if (obj[prop] === value) return true;
37 | obj[prop] = value;
38 | emit(data);
39 | return true;
40 | },
41 | deleteProperty (obj, prop) {
42 | delete obj[prop];
43 | emit(data);
44 | return true;
45 | }
46 | };
47 | }
48 |
49 | /**
50 | * Create a new store
51 | * @param {Object} data The data object
52 | * @return {Proxy} The reactive proxy
53 | */
54 | function store (data = {}) {
55 | data = ['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(data)) ? data : {value: data};
56 | return new Proxy(data, handler(data));
57 | }
58 |
59 | return store;
60 |
61 | })();
--------------------------------------------------------------------------------
/04 - components/01 - reactive component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Reactive Components
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
85 |
86 |
--------------------------------------------------------------------------------
/04 - components/02 - node selectors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Node Selectors
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
91 |
92 |
--------------------------------------------------------------------------------
/04 - components/03 - component methods/component.js:
--------------------------------------------------------------------------------
1 | let component = (function () {
2 |
3 | /**
4 | * Get an element in the DOM
5 | * @param {String|Node} elem The element or element selector
6 | * @return {Node} The DOM node
7 | */
8 | function getElem (elem) {
9 | return typeof elem === 'string' ? document.querySelector(elem) : elem;
10 | }
11 |
12 | /**
13 | * Component Class
14 | */
15 | class Component {
16 |
17 | /**
18 | * The constructor object
19 | * @param {Node|String} elem The element or selector to render the template into
20 | * @param {Function} html The template function to run when the data updates
21 | */
22 | constructor (elem, html) {
23 |
24 | // Create instance properties
25 | this.elem = elem;
26 | this.template = html;
27 | this.handler = function (event) {
28 | render(getElem(elem), html());
29 | };
30 |
31 | // Init
32 | this.start();
33 |
34 | }
35 |
36 | /**
37 | * Start reactive data rendering
38 | */
39 | start () {
40 | render(getElem(this.elem), this.template());
41 | document.addEventListener('store', this.handler);
42 | }
43 |
44 | /**
45 | * Stop reactive data rendering
46 | */
47 | stop () {
48 | document.removeEventListener('store', this.handler);
49 | }
50 |
51 | /**
52 | * Render the UI
53 | */
54 | render () {
55 | render(getElem(this.elem), this.template());
56 | }
57 |
58 | }
59 |
60 | /**
61 | * Create a reactive component
62 | * @param {Node} elem The element to render into
63 | * @param {Function} html The template to render
64 | */
65 | function component (elem, html) {
66 | return new Component(elem, html);
67 | }
68 |
69 | return component;
70 |
71 | })();
--------------------------------------------------------------------------------
/04 - components/03 - component methods/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Component Methods
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
65 |
66 |
--------------------------------------------------------------------------------
/04 - components/04 - lifecycle events/component.js:
--------------------------------------------------------------------------------
1 | let component = (function () {
2 |
3 | /**
4 | * Get an element in the DOM
5 | * @param {String|Node} elem The element or element selector
6 | * @return {Node} The DOM node
7 | */
8 | function getElem (elem) {
9 | return typeof elem === 'string' ? document.querySelector(elem) : elem;
10 | }
11 |
12 | /**
13 | * Emit a custom event
14 | * @param {String} type The event type
15 | * @param {Node} elem The element to emit the event on
16 | */
17 | function emit (type, elem) {
18 |
19 | // Create a new event
20 | let event = new CustomEvent(type, {
21 | bubbles: true,
22 | cancelable: true
23 | });
24 |
25 | // Dispatch the event
26 | return elem.dispatchEvent(event);
27 |
28 | }
29 |
30 | /**
31 | * Component Class
32 | */
33 | class Component {
34 |
35 | /**
36 | * The constructor object
37 | * @param {Node|String} elem The element or selector to render the template into
38 | * @param {Function} html The template function to run when the data updates
39 | */
40 | constructor (elem, html) {
41 |
42 | // Create instance properties
43 | let self = this;
44 | self.elem = elem;
45 | self.template = html;
46 | self.handler = function (event) {
47 | self.render();
48 | };
49 |
50 | // Init
51 | self.start();
52 |
53 | }
54 |
55 | /**
56 | * Start reactive data rendering
57 | */
58 | start () {
59 | this.render();
60 | document.addEventListener('store', this.handler);
61 | emit('start', getElem(this.elem));
62 | }
63 |
64 | /**
65 | * Stop reactive data rendering
66 | */
67 | stop () {
68 | document.removeEventListener('store', this.handler);
69 | emit('stop', getElem(this.elem));
70 | }
71 |
72 | /**
73 | * Render the UI
74 | */
75 | render () {
76 | let elem = getElem(this.elem);
77 | render(elem, this.template());
78 | emit('render', elem);
79 | }
80 |
81 | }
82 |
83 | /**
84 | * Create a reactive component
85 | * @param {Node} elem The element to render into
86 | * @param {Function} html The template to render
87 | */
88 | function component (elem, html) {
89 | return new Component(elem, html);
90 | }
91 |
92 | return component;
93 |
94 | })();
--------------------------------------------------------------------------------
/04 - components/04 - lifecycle events/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Lifecycle Events
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
74 |
75 |
--------------------------------------------------------------------------------
/04 - components/render.js:
--------------------------------------------------------------------------------
1 | let render = (function () {
2 |
3 | // Form fields and attributes that can be modified by users
4 | // They also have implicit values that make it hard to know if they were changed by the user or developer
5 | let formFields = ['input', 'option', 'textarea'];
6 | let formAtts = ['value', 'checked', 'selected'];
7 | let formAttsNoVal = ['checked', 'selected'];
8 |
9 | /**
10 | * Convert a template string into HTML DOM nodes
11 | * @param {String} str The template string
12 | * @return {Node} The template HTML
13 | */
14 | function stringToHTML (str) {
15 |
16 | // Create document
17 | let parser = new DOMParser();
18 | let doc = parser.parseFromString(str, 'text/html');
19 |
20 | // If there are items in the head, move them to the body
21 | if (doc.head && doc.head.childNodes.length) {
22 | Array.from(doc.head.childNodes).reverse().forEach(function (node) {
23 | doc.body.insertBefore(node, doc.body.firstChild);
24 | });
25 | }
26 |
27 | return doc.body || document.createElement('body');
28 |
29 | }
30 |
31 | /**
32 | * Check if an attribute string has a stringified falsy value
33 | * @param {String} str The string
34 | * @return {Boolean} If true, value is falsy (yea, I know, that's a little confusing)
35 | */
36 | function isFalsy (str) {
37 | return ['false', 'null', 'undefined', '0', '-0', 'NaN', '0n', '-0n'].includes(str);
38 | }
39 |
40 | /**
41 | * Check if attribute should be skipped (sanitize properties)
42 | * @param {String} name The attribute name
43 | * @param {String} value The attribute value
44 | * @return {Boolean} If true, skip the attribute
45 | */
46 | function skipAttribute (name, value) {
47 | let val = value.replace(/\s+/g, '').toLowerCase();
48 | if (['src', 'href', 'xlink:href'].includes(name)) {
49 | if (val.includes('javascript:') || val.includes('data:text/html')) return true;
50 | }
51 | if (name.startsWith('on')) return true;
52 | }
53 |
54 | /**
55 | * Add an attribute to an element
56 | * @param {Node} elem The element
57 | * @param {String} att The attribute
58 | * @param {String} val The value
59 | */
60 | function addAttribute (elem, att, val) {
61 |
62 | // Sanitize dangerous attributes
63 | if (skipAttribute(att, val)) return;
64 |
65 | // If it's a form attribute, set the property directly
66 | if (formAtts.includes(att)) {
67 | elem[att] = att === 'value' ? val : ' ';
68 | }
69 |
70 | // Update the attribute
71 | elem.setAttribute(att, val);
72 |
73 |
74 | }
75 |
76 | /**
77 | * Remove an attribute from an element
78 | * @param {Node} elem The element
79 | * @param {String} att The attribute
80 | */
81 | function removeAttribute (elem, att) {
82 |
83 | // If it's a form attribute, remove the property directly
84 | if (formAtts.includes(att)) {
85 | elem[att] = '';
86 | }
87 |
88 | // Remove the attribute
89 | elem.removeAttribute(att);
90 |
91 | }
92 |
93 | /**
94 | * Compare the existing node attributes to the template node attributes and make updates
95 | * @param {Node} template The new template
96 | * @param {Node} existing The existing DOM node
97 | */
98 | function diffAttributes (template, existing) {
99 |
100 | // If the node is not an element, bail
101 | if (template.nodeType !== 1) return;
102 |
103 | // Get attributes for the template and existing DOM
104 | let templateAtts = template.attributes;
105 | let existingAtts = existing.attributes;
106 |
107 | // Add and update attributes from the template into the DOM
108 | for (let {name, value} of templateAtts) {
109 |
110 | // Skip user-editable form field attributes
111 | if (formAtts.includes(name) && formFields.includes(template.tagName.toLowerCase())) continue;
112 |
113 | // If its a no-value property and it's falsy remove it
114 | if (formAttsNoVal.includes(name) && isFalsy(value)) {
115 | removeAttribute(existing, name);
116 | continue;
117 | }
118 |
119 | // Otherwise, add the attribute
120 | addAttribute(existing, name, value);
121 |
122 | }
123 |
124 | // Remove attributes from the DOM that shouldn't be there
125 | for (let {name, value} of existingAtts) {
126 |
127 | // If the attribute exists in the template, skip it
128 | if (templateAtts[name]) continue;
129 |
130 | // Skip user-editable form field attributes
131 | if (formAtts.includes(name) && formFields.includes(existing.tagName.toLowerCase())) continue;
132 |
133 | // Otherwise, remove it
134 | removeAttribute(existing, name);
135 |
136 | }
137 |
138 | }
139 |
140 | /**
141 | * Add default attributes to a newly created element
142 | * @param {Node} elem The element
143 | */
144 | function addDefaultAtts (elem) {
145 |
146 | // Only run on elements
147 | if (elem.nodeType !== 1) return;
148 |
149 | // Remove unsafe HTML attributes
150 | for (let {name, value} of elem.attributes) {
151 |
152 | // If the attribute should be skipped, remove it
153 | if (skipAttribute(name, value)) {
154 | removeAttribute(elem, name);
155 | continue;
156 | }
157 |
158 | // If it's a no-value attribute and its falsy, skip it
159 | if (formAttsNoVal.includes(name) && isFalsy(value)) continue;
160 |
161 | // Add the plain attribute
162 | addAttribute(elem, name, value);
163 |
164 | }
165 |
166 | // If there are child elems, recursively add defaults to them
167 | if (elem.childNodes) {
168 | for (let node of elem.childNodes) {
169 | addDefaultAtts(node);
170 | }
171 | }
172 |
173 | }
174 |
175 | /**
176 | * Get the content from a node
177 | * @param {Node} node The node
178 | * @return {String} The content
179 | */
180 | function getNodeContent (node) {
181 | return node.childNodes && node.childNodes.length ? null : node.textContent;
182 | }
183 |
184 | /**
185 | * Check if two nodes are different
186 | * @param {Node} node1 The first node
187 | * @param {Node} node2 The second node
188 | * @return {Boolean} If true, they're not the same node
189 | */
190 | function isDifferentNode (node1, node2) {
191 | return (
192 | (typeof node1.nodeType === 'number' && node1.nodeType !== node2.nodeType) ||
193 | (typeof node1.tagName === 'string' && node1.tagName !== node2.tagName) ||
194 | (typeof node1.id === 'string' && node1.id !== node2.id) ||
195 | (typeof node1.src === 'string' && node1.src !== node2.src)
196 | );
197 | }
198 |
199 | /**
200 | * Check if the desired node is further ahead in the DOM existingNodes
201 | * @param {Node} node The node to look for
202 | * @param {NodeList} existingNodes The DOM existingNodes
203 | * @param {Integer} index The indexing index
204 | * @return {Integer} How many nodes ahead the target node is
205 | */
206 | function aheadInTree (node, existingNodes, index) {
207 | return Array.from(existingNodes).slice(index + 1).find(function (branch) {
208 | return !isDifferentNode(node, branch);
209 | });
210 | }
211 |
212 | /**
213 | * If there are extra elements in DOM, remove them
214 | * @param {Array} existingNodes The existing DOM
215 | * @param {Array} templateNodes The template
216 | */
217 | function trimExtraNodes (existingNodes, templateNodes) {
218 | let extra = existingNodes.length - templateNodes.length;
219 | if (extra < 1) return;
220 | for (; extra > 0; extra--) {
221 | existingNodes[existingNodes.length - 1].remove();
222 | }
223 | }
224 |
225 | /**
226 | * Remove scripts from HTML
227 | * @param {Node} elem The element to remove scripts from
228 | */
229 | function removeScripts (elem) {
230 | let scripts = elem.querySelectorAll('script');
231 | for (let script of scripts) {
232 | script.remove();
233 | }
234 | }
235 |
236 | /**
237 | * Diff the existing DOM node versus the template
238 | * @param {Array} template The template HTML
239 | * @param {Node} existing The current DOM HTML
240 | */
241 | function diff (template, existing) {
242 |
243 | // Get the nodes in the template and existing UI
244 | let templateNodes = template.childNodes;
245 | let existingNodes = existing.childNodes;
246 |
247 | // Don't inject scripts
248 | if (removeScripts(template)) return;
249 |
250 | // Loop through each node in the template and compare it to the matching element in the UI
251 | templateNodes.forEach(function (node, index) {
252 |
253 | // If element doesn't exist, create it
254 | if (!existingNodes[index]) {
255 | let clone = node.cloneNode(true);
256 | addDefaultAtts(clone);
257 | existing.append(clone);
258 | return;
259 | }
260 |
261 | // If there is, but it's not the same node type, insert the new node before the existing one
262 | if (isDifferentNode(node, existingNodes[index])) {
263 |
264 | // Check if node exists further in the tree
265 | let ahead = aheadInTree(node, existingNodes, index);
266 |
267 | // If not, insert the node before the current one
268 | if (!ahead) {
269 | let clone = node.cloneNode(true);
270 | addDefaultAtts(clone);
271 | existingNodes[index].before(clone);
272 | return;
273 | }
274 |
275 | // Otherwise, move it to the current spot
276 | existingNodes[index].before(ahead);
277 |
278 | }
279 |
280 | // If attributes are different, update them
281 | diffAttributes(node, existingNodes[index]);
282 |
283 | // Stop diffing if a native web component
284 | if (node.nodeName.includes('-')) return;
285 |
286 | // If content is different, update it
287 | let templateContent = getNodeContent(node);
288 | if (templateContent && templateContent !== getNodeContent(existingNodes[index])) {
289 | existingNodes[index].textContent = templateContent;
290 | }
291 |
292 | // If there shouldn't be child nodes but there are, remove them
293 | if (!node.childNodes.length && existingNodes[index].childNodes.length) {
294 | existingNodes[index].innerHTML = '';
295 | return;
296 | }
297 |
298 | // If DOM is empty and shouldn't be, build it up
299 | // This uses a document fragment to minimize reflows
300 | if (!existingNodes[index].childNodes.length && node.childNodes.length) {
301 | let fragment = document.createDocumentFragment();
302 | diff(node, fragment);
303 | existingNodes[index].appendChild(fragment);
304 | return;
305 | }
306 |
307 | // If there are nodes within it, recursively diff those
308 | if (node.childNodes.length) {
309 | diff(node, existingNodes[index]);
310 | }
311 |
312 | });
313 |
314 | // If extra elements in DOM, remove them
315 | trimExtraNodes(existingNodes, templateNodes);
316 |
317 | }
318 |
319 | /**
320 | * Render a template into the UI
321 | * @param {Node|String} elem The element or selector to render the template into
322 | * @param {String} template The template to render
323 | */
324 | function render (elem, template) {
325 | let node = elem === 'string' ? document.querySelector(elem) : elem;
326 | let html = stringToHTML(template);
327 | diff(html, node);
328 | }
329 |
330 | return render;
331 |
332 | })();
--------------------------------------------------------------------------------
/04 - components/store.js:
--------------------------------------------------------------------------------
1 | let store = (function () {
2 |
3 | /**
4 | * Emit a custom event
5 | * @param {*} data The store data
6 | */
7 | function emit (data) {
8 |
9 | // Create a new event
10 | let event = new CustomEvent('store', {
11 | bubbles: true,
12 | cancelable: true,
13 | detail: data
14 | });
15 |
16 | // Dispatch the event
17 | return document.dispatchEvent(event);
18 |
19 | }
20 |
21 | /**
22 | * Create a Proxy handler object
23 | * @param {Object} data The data object
24 | * @return {Object} The handler object
25 | */
26 | function handler (data) {
27 | return {
28 | get (obj, prop) {
29 | if (prop === '_isProxy') return true;
30 | if (['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(obj[prop])) && !obj[prop]._isProxy) {
31 | obj[prop] = new Proxy(obj[prop], handler(data));
32 | }
33 | return obj[prop];
34 | },
35 | set (obj, prop, value) {
36 | if (obj[prop] === value) return true;
37 | obj[prop] = value;
38 | emit(data);
39 | return true;
40 | },
41 | deleteProperty (obj, prop) {
42 | delete obj[prop];
43 | emit(data);
44 | return true;
45 | }
46 | };
47 | }
48 |
49 | /**
50 | * Create a new store
51 | * @param {Object} data The data object
52 | * @return {Proxy} The reactive proxy
53 | */
54 | function store (data = {}) {
55 | data = ['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(data)) ? data : {value: data};
56 | return new Proxy(data, handler(data));
57 | }
58 |
59 | return store;
60 |
61 | })();
--------------------------------------------------------------------------------
/05 - performance/01 - batching renders.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Batching Renders
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
72 |
73 |
--------------------------------------------------------------------------------
/05 - performance/02 - memoization and ids.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Memoization & IDs
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
64 |
65 |
--------------------------------------------------------------------------------
/05 - performance/03 - virtual dom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Virtual DOM
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 | Wizards
21 |
22 | - Gandalf
23 | - Radagast
24 | - Merlin
25 |
26 |
27 |
28 |
69 |
70 |
--------------------------------------------------------------------------------
/05 - performance/04 - why use a virtual dom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Why use a virtual DOM?
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Reply
35 |
36 |
37 | Retweet
38 |
39 |
40 | Favorite
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/05 - performance/component.js:
--------------------------------------------------------------------------------
1 | let component = (function () {
2 |
3 | /**
4 | * Get an element in the DOM
5 | * @param {String|Node} elem The element or element selector
6 | * @return {Node} The DOM node
7 | */
8 | function getElem (elem) {
9 | return typeof elem === 'string' ? document.querySelector(elem) : elem;
10 | }
11 |
12 | /**
13 | * Emit a custom event
14 | * @param {String} type The event type
15 | * @param {Node} elem The element to emit the event on
16 | */
17 | function emit (type, elem) {
18 |
19 | // Create a new event
20 | let event = new CustomEvent(type, {
21 | bubbles: true,
22 | cancelable: true
23 | });
24 |
25 | // Dispatch the event
26 | return elem.dispatchEvent(event);
27 |
28 | }
29 |
30 | /**
31 | * Component Class
32 | */
33 | class Component {
34 |
35 | /**
36 | * The constructor object
37 | * @param {Node|String} elem The element or selector to render the template into
38 | * @param {Function} html The template function to run when the data updates
39 | */
40 | constructor (elem, html) {
41 |
42 | // Create instance properties
43 | let self = this;
44 | self.elem = elem;
45 | self.template = html;
46 | self.handler = function (event) {
47 | self.render();
48 | };
49 | self.debounce = null;
50 |
51 | // Init
52 | self.start();
53 |
54 | }
55 |
56 | /**
57 | * Start reactive data rendering
58 | */
59 | start () {
60 | this.render();
61 | document.addEventListener('store', this.handler);
62 | emit('start', getElem(this.elem));
63 | }
64 |
65 | /**
66 | * Stop reactive data rendering
67 | */
68 | stop () {
69 | document.removeEventListener('store', this.handler);
70 | emit('stop', getElem(this.elem));
71 | }
72 |
73 | /**
74 | * Render the UI
75 | */
76 | render () {
77 |
78 | // Cache instance
79 | let self = this;
80 |
81 | // If there's a pending render, cancel it
82 | if (self.debounce) {
83 | window.cancelAnimationFrame(self.debounce);
84 | }
85 |
86 | // Setup the new render to run at the next animation frame
87 | self.debounce = window.requestAnimationFrame(function () {
88 | let elem = getElem(self.elem);
89 | render(elem, self.template());
90 | emit('render', elem);
91 | });
92 |
93 | }
94 |
95 | }
96 |
97 | /**
98 | * Create a reactive component
99 | * @param {Node} elem The element to render into
100 | * @param {Function} html The template to render
101 | */
102 | function component (elem, html) {
103 | return new Component(elem, html);
104 | }
105 |
106 | return component;
107 |
108 | })();
--------------------------------------------------------------------------------
/05 - performance/render.js:
--------------------------------------------------------------------------------
1 | let render = (function () {
2 |
3 | // Form fields and attributes that can be modified by users
4 | // They also have implicit values that make it hard to know if they were changed by the user or developer
5 | let formFields = ['input', 'option', 'textarea'];
6 | let formAtts = ['value', 'checked', 'selected'];
7 | let formAttsNoVal = ['checked', 'selected'];
8 |
9 | /**
10 | * Convert a template string into HTML DOM nodes
11 | * @param {String} str The template string
12 | * @return {Node} The template HTML
13 | */
14 | function stringToHTML (str) {
15 |
16 | // Create document
17 | let parser = new DOMParser();
18 | let doc = parser.parseFromString(str, 'text/html');
19 |
20 | // If there are items in the head, move them to the body
21 | if (doc.head && doc.head.childNodes.length) {
22 | Array.from(doc.head.childNodes).reverse().forEach(function (node) {
23 | doc.body.insertBefore(node, doc.body.firstChild);
24 | });
25 | }
26 |
27 | return doc.body || document.createElement('body');
28 |
29 | }
30 |
31 | /**
32 | * Check if an attribute string has a stringified falsy value
33 | * @param {String} str The string
34 | * @return {Boolean} If true, value is falsy (yea, I know, that's a little confusing)
35 | */
36 | function isFalsy (str) {
37 | return ['false', 'null', 'undefined', '0', '-0', 'NaN', '0n', '-0n'].includes(str);
38 | }
39 |
40 | /**
41 | * Check if attribute should be skipped (sanitize properties)
42 | * @param {String} name The attribute name
43 | * @param {String} value The attribute value
44 | * @return {Boolean} If true, skip the attribute
45 | */
46 | function skipAttribute (name, value) {
47 | let val = value.replace(/\s+/g, '').toLowerCase();
48 | if (['src', 'href', 'xlink:href'].includes(name)) {
49 | if (val.includes('javascript:') || val.includes('data:text/html')) return true;
50 | }
51 | if (name.startsWith('on')) return true;
52 | }
53 |
54 | /**
55 | * Add an attribute to an element
56 | * @param {Node} elem The element
57 | * @param {String} att The attribute
58 | * @param {String} val The value
59 | */
60 | function addAttribute (elem, att, val) {
61 |
62 | // Sanitize dangerous attributes
63 | if (skipAttribute(att, val)) return;
64 |
65 | // If it's a form attribute, set the property directly
66 | if (formAtts.includes(att)) {
67 | elem[att] = att === 'value' ? val : ' ';
68 | }
69 |
70 | // Update the attribute
71 | elem.setAttribute(att, val);
72 |
73 |
74 | }
75 |
76 | /**
77 | * Remove an attribute from an element
78 | * @param {Node} elem The element
79 | * @param {String} att The attribute
80 | */
81 | function removeAttribute (elem, att) {
82 |
83 | // If it's a form attribute, remove the property directly
84 | if (formAtts.includes(att)) {
85 | elem[att] = '';
86 | }
87 |
88 | // Remove the attribute
89 | elem.removeAttribute(att);
90 |
91 | }
92 |
93 | /**
94 | * Compare the existing node attributes to the template node attributes and make updates
95 | * @param {Node} template The new template
96 | * @param {Node} existing The existing DOM node
97 | */
98 | function diffAttributes (template, existing) {
99 |
100 | // If the node is not an element, bail
101 | if (template.nodeType !== 1) return;
102 |
103 | // Get attributes for the template and existing DOM
104 | let templateAtts = template.attributes;
105 | let existingAtts = existing.attributes;
106 |
107 | // Add and update attributes from the template into the DOM
108 | for (let {name, value} of templateAtts) {
109 |
110 | // Skip user-editable form field attributes
111 | if (formAtts.includes(name) && formFields.includes(template.tagName.toLowerCase())) continue;
112 |
113 | // If its a no-value property and it's falsy remove it
114 | if (formAttsNoVal.includes(name) && isFalsy(value)) {
115 | removeAttribute(existing, name);
116 | continue;
117 | }
118 |
119 | // Otherwise, add the attribute
120 | addAttribute(existing, name, value);
121 |
122 | }
123 |
124 | // Remove attributes from the DOM that shouldn't be there
125 | for (let {name, value} of existingAtts) {
126 |
127 | // If the attribute exists in the template, skip it
128 | if (templateAtts[name]) continue;
129 |
130 | // Skip user-editable form field attributes
131 | if (formAtts.includes(name) && formFields.includes(existing.tagName.toLowerCase())) continue;
132 |
133 | // Otherwise, remove it
134 | removeAttribute(existing, name);
135 |
136 | }
137 |
138 | }
139 |
140 | /**
141 | * Add default attributes to a newly created element
142 | * @param {Node} elem The element
143 | */
144 | function addDefaultAtts (elem) {
145 |
146 | // Only run on elements
147 | if (elem.nodeType !== 1) return;
148 |
149 | // Remove unsafe HTML attributes
150 | for (let {name, value} of elem.attributes) {
151 |
152 | // If the attribute should be skipped, remove it
153 | if (skipAttribute(name, value)) {
154 | removeAttribute(elem, name);
155 | continue;
156 | }
157 |
158 | // If it's a no-value attribute and its falsy, skip it
159 | if (formAttsNoVal.includes(name) && isFalsy(value)) continue;
160 |
161 | // Add the plain attribute
162 | addAttribute(elem, name, value);
163 |
164 | }
165 |
166 | // If there are child elems, recursively add defaults to them
167 | if (elem.childNodes) {
168 | for (let node of elem.childNodes) {
169 | addDefaultAtts(node);
170 | }
171 | }
172 |
173 | }
174 |
175 | /**
176 | * Get the content from a node
177 | * @param {Node} node The node
178 | * @return {String} The content
179 | */
180 | function getNodeContent (node) {
181 | return node.childNodes && node.childNodes.length ? null : node.textContent;
182 | }
183 |
184 | /**
185 | * Check if two nodes are different
186 | * @param {Node} node1 The first node
187 | * @param {Node} node2 The second node
188 | * @return {Boolean} If true, they're not the same node
189 | */
190 | function isDifferentNode (node1, node2) {
191 | return (
192 | (typeof node1.nodeType === 'number' && node1.nodeType !== node2.nodeType) ||
193 | (typeof node1.tagName === 'string' && node1.tagName !== node2.tagName) ||
194 | (typeof node1.id === 'string' && node1.id !== node2.id) ||
195 | (typeof node1.src === 'string' && node1.src !== node2.src)
196 | );
197 | }
198 |
199 | /**
200 | * Check if the desired node is further ahead in the DOM existingNodes
201 | * @param {Node} node The node to look for
202 | * @param {NodeList} existingNodes The DOM existingNodes
203 | * @param {Integer} index The indexing index
204 | * @return {Integer} How many nodes ahead the target node is
205 | */
206 | function aheadInTree (node, existingNodes, index) {
207 | return Array.from(existingNodes).slice(index + 1).find(function (branch) {
208 | return !isDifferentNode(node, branch);
209 | });
210 | }
211 |
212 | /**
213 | * If there are extra elements in DOM, remove them
214 | * @param {Array} existingNodes The existing DOM
215 | * @param {Array} templateNodes The template
216 | */
217 | function trimExtraNodes (existingNodes, templateNodes) {
218 | let extra = existingNodes.length - templateNodes.length;
219 | if (extra < 1) return;
220 | for (; extra > 0; extra--) {
221 | existingNodes[existingNodes.length - 1].remove();
222 | }
223 | }
224 |
225 | /**
226 | * Remove scripts from HTML
227 | * @param {Node} elem The element to remove scripts from
228 | */
229 | function removeScripts (elem) {
230 | let scripts = elem.querySelectorAll('script');
231 | for (let script of scripts) {
232 | script.remove();
233 | }
234 | }
235 |
236 | /**
237 | * Diff the existing DOM node versus the template
238 | * @param {Array} template The template HTML
239 | * @param {Node} existing The current DOM HTML
240 | */
241 | function diff (template, existing) {
242 |
243 | // Get the nodes in the template and existing UI
244 | let templateNodes = template.childNodes;
245 | let existingNodes = existing.childNodes;
246 |
247 | // Don't inject scripts
248 | if (removeScripts(template)) return;
249 |
250 | // Loop through each node in the template and compare it to the matching element in the UI
251 | templateNodes.forEach(function (node, index) {
252 |
253 | // If element doesn't exist, create it
254 | if (!existingNodes[index]) {
255 | let clone = node.cloneNode(true);
256 | addDefaultAtts(clone);
257 | existing.append(clone);
258 | return;
259 | }
260 |
261 | // If there is, but it's not the same node type, insert the new node before the existing one
262 | if (isDifferentNode(node, existingNodes[index])) {
263 |
264 | // Check if node exists further in the tree
265 | let ahead = aheadInTree(node, existingNodes, index);
266 |
267 | // If not, insert the node before the current one
268 | if (!ahead) {
269 | let clone = node.cloneNode(true);
270 | addDefaultAtts(clone);
271 | existingNodes[index].before(clone);
272 | return;
273 | }
274 |
275 | // Otherwise, move it to the current spot
276 | existingNodes[index].before(ahead);
277 |
278 | }
279 |
280 | // If attributes are different, update them
281 | diffAttributes(node, existingNodes[index]);
282 |
283 | // Stop diffing if a native web component
284 | if (node.nodeName.includes('-')) return;
285 |
286 | // If content is different, update it
287 | let templateContent = getNodeContent(node);
288 | if (templateContent && templateContent !== getNodeContent(existingNodes[index])) {
289 | existingNodes[index].textContent = templateContent;
290 | }
291 |
292 | // If there shouldn't be child nodes but there are, remove them
293 | if (!node.childNodes.length && existingNodes[index].childNodes.length) {
294 | existingNodes[index].innerHTML = '';
295 | return;
296 | }
297 |
298 | // If DOM is empty and shouldn't be, build it up
299 | // This uses a document fragment to minimize reflows
300 | if (!existingNodes[index].childNodes.length && node.childNodes.length) {
301 | let fragment = document.createDocumentFragment();
302 | diff(node, fragment);
303 | existingNodes[index].appendChild(fragment);
304 | return;
305 | }
306 |
307 | // If there are nodes within it, recursively diff those
308 | if (node.childNodes.length) {
309 | diff(node, existingNodes[index]);
310 | }
311 |
312 | });
313 |
314 | // If extra elements in DOM, remove them
315 | trimExtraNodes(existingNodes, templateNodes);
316 |
317 | }
318 |
319 | /**
320 | * Render a template into the UI
321 | * @param {Node|String} elem The element or selector to render the template into
322 | * @param {String} template The template to render
323 | */
324 | function render (elem, template) {
325 | let node = elem === 'string' ? document.querySelector(elem) : elem;
326 | let html = stringToHTML(template);
327 | diff(html, node);
328 | }
329 |
330 | return render;
331 |
332 | })();
--------------------------------------------------------------------------------
/05 - performance/store.js:
--------------------------------------------------------------------------------
1 | let store = (function () {
2 |
3 | /**
4 | * Emit a custom event
5 | * @param {*} data The store data
6 | */
7 | function emit (data) {
8 |
9 | // Create a new event
10 | let event = new CustomEvent('store', {
11 | bubbles: true,
12 | cancelable: true,
13 | detail: data
14 | });
15 |
16 | // Dispatch the event
17 | return document.dispatchEvent(event);
18 |
19 | }
20 |
21 | /**
22 | * Create a Proxy handler object
23 | * @param {Object} data The data object
24 | * @return {Object} The handler object
25 | */
26 | function handler (data) {
27 | return {
28 | get (obj, prop) {
29 | if (prop === '_isProxy') return true;
30 | if (['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(obj[prop])) && !obj[prop]._isProxy) {
31 | obj[prop] = new Proxy(obj[prop], handler(data));
32 | }
33 | return obj[prop];
34 | },
35 | set (obj, prop, value) {
36 | if (obj[prop] === value) return true;
37 | obj[prop] = value;
38 | emit(data);
39 | return true;
40 | },
41 | deleteProperty (obj, prop) {
42 | delete obj[prop];
43 | emit(data);
44 | return true;
45 | }
46 | };
47 | }
48 |
49 | /**
50 | * Create a new store
51 | * @param {Object} data The data object
52 | * @return {Proxy} The reactive proxy
53 | */
54 | function store (data = {}) {
55 | data = ['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(data)) ? data : {value: data};
56 | return new Proxy(data, handler(data));
57 | }
58 |
59 | return store;
60 |
61 | })();
--------------------------------------------------------------------------------
/06 - project/01 - project template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Project Template
7 |
8 |
9 |
36 |
37 |
38 |
39 |
40 | Awesome Dogs
41 |
42 |
43 |
44 |
45 |
51 |
52 |
53 |
54 |
55 |
61 |
62 |
63 |
64 |
65 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
84 |
85 |
--------------------------------------------------------------------------------
/06 - project/02 - project completed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Project Completed
7 |
8 |
9 |
44 |
45 |
46 |
47 |
48 | Awesome Dogs
49 |
50 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
69 |
70 |
71 |
72 |
73 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/06 - project/component.js:
--------------------------------------------------------------------------------
1 | let component = (function () {
2 |
3 | /**
4 | * Get an element in the DOM
5 | * @param {String|Node} elem The element or element selector
6 | * @return {Node} The DOM node
7 | */
8 | function getElem (elem) {
9 | return typeof elem === 'string' ? document.querySelector(elem) : elem;
10 | }
11 |
12 | /**
13 | * Emit a custom event
14 | * @param {String} type The event type
15 | * @param {Node} elem The element to emit the event on
16 | */
17 | function emit (type, elem) {
18 |
19 | // Create a new event
20 | let event = new CustomEvent(type, {
21 | bubbles: true,
22 | cancelable: true
23 | });
24 |
25 | // Dispatch the event
26 | return elem.dispatchEvent(event);
27 |
28 | }
29 |
30 | /**
31 | * Component Class
32 | */
33 | class Component {
34 |
35 | /**
36 | * The constructor object
37 | * @param {Node|String} elem The element or selector to render the template into
38 | * @param {Function} html The template function to run when the data updates
39 | */
40 | constructor (elem, html) {
41 |
42 | // Create instance properties
43 | let self = this;
44 | self.elem = elem;
45 | self.template = html;
46 | self.handler = function (event) {
47 | self.render();
48 | };
49 | self.debounce = null;
50 |
51 | // Init
52 | self.start();
53 |
54 | }
55 |
56 | /**
57 | * Start reactive data rendering
58 | */
59 | start () {
60 | this.render();
61 | document.addEventListener('store', this.handler);
62 | emit('start', getElem(this.elem));
63 | }
64 |
65 | /**
66 | * Stop reactive data rendering
67 | */
68 | stop () {
69 | document.removeEventListener('store', this.handler);
70 | emit('stop', getElem(this.elem));
71 | }
72 |
73 | /**
74 | * Render the UI
75 | */
76 | render () {
77 |
78 | // Cache instance
79 | let self = this;
80 |
81 | // If there's a pending render, cancel it
82 | if (self.debounce) {
83 | window.cancelAnimationFrame(self.debounce);
84 | }
85 |
86 | // Setup the new render to run at the next animation frame
87 | self.debounce = window.requestAnimationFrame(function () {
88 | let elem = getElem(self.elem);
89 | render(elem, self.template());
90 | emit('render', elem);
91 | });
92 |
93 | }
94 |
95 | }
96 |
97 | /**
98 | * Create a reactive component
99 | * @param {Node} elem The element to render into
100 | * @param {Function} html The template to render
101 | */
102 | function component (elem, html) {
103 | return new Component(elem, html);
104 | }
105 |
106 | return component;
107 |
108 | })();
--------------------------------------------------------------------------------
/06 - project/dogs.js:
--------------------------------------------------------------------------------
1 | let dogData = [
2 | {
3 | name: 'Bandit',
4 | age: 'Adult',
5 | size: 'Large',
6 | cats: true,
7 | img: 'bandit.jpg'
8 | },
9 | {
10 | name: 'Bert',
11 | age: 'Adult',
12 | size: 'Small',
13 | cats: false,
14 | img: 'bert.jpg'
15 | },
16 | {
17 | name: 'Brad',
18 | age: 'Puppy',
19 | size: 'Large',
20 | cats: false,
21 | img: 'brad.jpg'
22 | },
23 | {
24 | name: 'Chewie',
25 | age: 'Senior',
26 | size: 'Small',
27 | cats: false,
28 | img: 'chewie.jpg'
29 | },
30 | {
31 | name: 'Daphne',
32 | age: 'Adult',
33 | size: 'Large',
34 | cats: true,
35 | img: 'daphne.jpg'
36 | },
37 | {
38 | name: 'Ethel',
39 | age: 'Puppy',
40 | size: 'Large',
41 | cats: true,
42 | img: 'ethel.jpg'
43 | },
44 | {
45 | name: 'Florence',
46 | age: 'Adult',
47 | size: 'Small',
48 | cats: false,
49 | img: 'florence.jpg'
50 | },
51 | {
52 | name: 'Grace',
53 | age: 'Puppy',
54 | size: 'Small',
55 | cats: false,
56 | img: 'grace.jpg'
57 | },
58 | {
59 | name: 'Janice',
60 | age: 'Senior',
61 | size: 'Medium',
62 | cats: true,
63 | img: 'janice.jpg'
64 | },
65 | {
66 | name: 'Joseph',
67 | age: 'Senior',
68 | size: 'Small',
69 | cats: true,
70 | img: 'joseph.jpg'
71 | },
72 | {
73 | name: 'Kyle',
74 | age: 'Puppy',
75 | size: 'Medium',
76 | cats: true,
77 | img: 'kyle.jpg'
78 | },
79 | {
80 | name: 'Reba',
81 | age: 'Senior',
82 | size: 'Large',
83 | cats: true,
84 | img: 'reba.jpg'
85 | },
86 | {
87 | name: 'Thor',
88 | age: 'Puppy',
89 | size: 'Large',
90 | cats: true,
91 | img: 'thor.jpg'
92 | },
93 | {
94 | name: 'William',
95 | age: 'Senior',
96 | size: 'Medium',
97 | cats: true,
98 | img: 'william.jpg'
99 | }
100 | ];
--------------------------------------------------------------------------------
/06 - project/img/bandit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/bandit.jpg
--------------------------------------------------------------------------------
/06 - project/img/bert.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/bert.jpg
--------------------------------------------------------------------------------
/06 - project/img/brad.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/brad.jpg
--------------------------------------------------------------------------------
/06 - project/img/chewie.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/chewie.jpg
--------------------------------------------------------------------------------
/06 - project/img/daphne.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/daphne.jpg
--------------------------------------------------------------------------------
/06 - project/img/ethel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/ethel.jpg
--------------------------------------------------------------------------------
/06 - project/img/florence.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/florence.jpg
--------------------------------------------------------------------------------
/06 - project/img/grace.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/grace.jpg
--------------------------------------------------------------------------------
/06 - project/img/janice.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/janice.jpg
--------------------------------------------------------------------------------
/06 - project/img/joseph.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/joseph.jpg
--------------------------------------------------------------------------------
/06 - project/img/kyle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/kyle.jpg
--------------------------------------------------------------------------------
/06 - project/img/reba.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/reba.jpg
--------------------------------------------------------------------------------
/06 - project/img/thor.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/thor.jpg
--------------------------------------------------------------------------------
/06 - project/img/william.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cferdinandi/state-based-ui-source-code/5856f4d885ed8f70d91bddc28d0cf2db1982e081/06 - project/img/william.jpg
--------------------------------------------------------------------------------
/06 - project/render.js:
--------------------------------------------------------------------------------
1 | let render = (function () {
2 |
3 | // Form fields and attributes that can be modified by users
4 | // They also have implicit values that make it hard to know if they were changed by the user or developer
5 | let formFields = ['input', 'option', 'textarea'];
6 | let formAtts = ['value', 'checked', 'selected'];
7 | let formAttsNoVal = ['checked', 'selected'];
8 |
9 | /**
10 | * Convert a template string into HTML DOM nodes
11 | * @param {String} str The template string
12 | * @return {Node} The template HTML
13 | */
14 | function stringToHTML (str) {
15 |
16 | // Create document
17 | let parser = new DOMParser();
18 | let doc = parser.parseFromString(str, 'text/html');
19 |
20 | // If there are items in the head, move them to the body
21 | if (doc.head && doc.head.childNodes.length) {
22 | Array.from(doc.head.childNodes).reverse().forEach(function (node) {
23 | doc.body.insertBefore(node, doc.body.firstChild);
24 | });
25 | }
26 |
27 | return doc.body || document.createElement('body');
28 |
29 | }
30 |
31 | /**
32 | * Check if an attribute string has a stringified falsy value
33 | * @param {String} str The string
34 | * @return {Boolean} If true, value is falsy (yea, I know, that's a little confusing)
35 | */
36 | function isFalsy (str) {
37 | return ['false', 'null', 'undefined', '0', '-0', 'NaN', '0n', '-0n'].includes(str);
38 | }
39 |
40 | /**
41 | * Check if attribute should be skipped (sanitize properties)
42 | * @param {String} name The attribute name
43 | * @param {String} value The attribute value
44 | * @return {Boolean} If true, skip the attribute
45 | */
46 | function skipAttribute (name, value) {
47 | let val = value.replace(/\s+/g, '').toLowerCase();
48 | if (['src', 'href', 'xlink:href'].includes(name)) {
49 | if (val.includes('javascript:') || val.includes('data:text/html')) return true;
50 | }
51 | if (name.startsWith('on')) return true;
52 | }
53 |
54 | /**
55 | * Add an attribute to an element
56 | * @param {Node} elem The element
57 | * @param {String} att The attribute
58 | * @param {String} val The value
59 | */
60 | function addAttribute (elem, att, val) {
61 |
62 | // Sanitize dangerous attributes
63 | if (skipAttribute(att, val)) return;
64 |
65 | // If it's a form attribute, set the property directly
66 | if (formAtts.includes(att)) {
67 | elem[att] = att === 'value' ? val : ' ';
68 | }
69 |
70 | // Update the attribute
71 | elem.setAttribute(att, val);
72 |
73 |
74 | }
75 |
76 | /**
77 | * Remove an attribute from an element
78 | * @param {Node} elem The element
79 | * @param {String} att The attribute
80 | */
81 | function removeAttribute (elem, att) {
82 |
83 | // If it's a form attribute, remove the property directly
84 | if (formAtts.includes(att)) {
85 | elem[att] = '';
86 | }
87 |
88 | // Remove the attribute
89 | elem.removeAttribute(att);
90 |
91 | }
92 |
93 | /**
94 | * Compare the existing node attributes to the template node attributes and make updates
95 | * @param {Node} template The new template
96 | * @param {Node} existing The existing DOM node
97 | */
98 | function diffAttributes (template, existing) {
99 |
100 | // If the node is not an element, bail
101 | if (template.nodeType !== 1) return;
102 |
103 | // Get attributes for the template and existing DOM
104 | let templateAtts = template.attributes;
105 | let existingAtts = existing.attributes;
106 |
107 | // Add and update attributes from the template into the DOM
108 | for (let {name, value} of templateAtts) {
109 |
110 | // Skip user-editable form field attributes
111 | if (formAtts.includes(name) && formFields.includes(template.tagName.toLowerCase())) continue;
112 |
113 | // If its a no-value property and it's falsy remove it
114 | if (formAttsNoVal.includes(name) && isFalsy(value)) {
115 | removeAttribute(existing, name);
116 | continue;
117 | }
118 |
119 | // Otherwise, add the attribute
120 | addAttribute(existing, name, value);
121 |
122 | }
123 |
124 | // Remove attributes from the DOM that shouldn't be there
125 | for (let {name, value} of existingAtts) {
126 |
127 | // If the attribute exists in the template, skip it
128 | if (templateAtts[name]) continue;
129 |
130 | // Skip user-editable form field attributes
131 | if (formAtts.includes(name) && formFields.includes(existing.tagName.toLowerCase())) continue;
132 |
133 | // Otherwise, remove it
134 | removeAttribute(existing, name);
135 |
136 | }
137 |
138 | }
139 |
140 | /**
141 | * Add default attributes to a newly created element
142 | * @param {Node} elem The element
143 | */
144 | function addDefaultAtts (elem) {
145 |
146 | // Only run on elements
147 | if (elem.nodeType !== 1) return;
148 |
149 | // Remove unsafe HTML attributes
150 | for (let {name, value} of elem.attributes) {
151 |
152 | // If the attribute should be skipped, remove it
153 | if (skipAttribute(name, value)) {
154 | removeAttribute(elem, name);
155 | continue;
156 | }
157 |
158 | // If it's a no-value attribute and its falsy, skip it
159 | if (formAttsNoVal.includes(name) && isFalsy(value)) continue;
160 |
161 | // Add the plain attribute
162 | addAttribute(elem, name, value);
163 |
164 | }
165 |
166 | // If there are child elems, recursively add defaults to them
167 | if (elem.childNodes) {
168 | for (let node of elem.childNodes) {
169 | addDefaultAtts(node);
170 | }
171 | }
172 |
173 | }
174 |
175 | /**
176 | * Get the content from a node
177 | * @param {Node} node The node
178 | * @return {String} The content
179 | */
180 | function getNodeContent (node) {
181 | return node.childNodes && node.childNodes.length ? null : node.textContent;
182 | }
183 |
184 | /**
185 | * Check if two nodes are different
186 | * @param {Node} node1 The first node
187 | * @param {Node} node2 The second node
188 | * @return {Boolean} If true, they're not the same node
189 | */
190 | function isDifferentNode (node1, node2) {
191 | return (
192 | (typeof node1.nodeType === 'number' && node1.nodeType !== node2.nodeType) ||
193 | (typeof node1.tagName === 'string' && node1.tagName !== node2.tagName) ||
194 | (typeof node1.id === 'string' && node1.id !== node2.id) ||
195 | (typeof node1.src === 'string' && node1.src !== node2.src)
196 | );
197 | }
198 |
199 | /**
200 | * Check if the desired node is further ahead in the DOM existingNodes
201 | * @param {Node} node The node to look for
202 | * @param {NodeList} existingNodes The DOM existingNodes
203 | * @param {Integer} index The indexing index
204 | * @return {Integer} How many nodes ahead the target node is
205 | */
206 | function aheadInTree (node, existingNodes, index) {
207 | return Array.from(existingNodes).slice(index + 1).find(function (branch) {
208 | return !isDifferentNode(node, branch);
209 | });
210 | }
211 |
212 | /**
213 | * If there are extra elements in DOM, remove them
214 | * @param {Array} existingNodes The existing DOM
215 | * @param {Array} templateNodes The template
216 | */
217 | function trimExtraNodes (existingNodes, templateNodes) {
218 | let extra = existingNodes.length - templateNodes.length;
219 | if (extra < 1) return;
220 | for (; extra > 0; extra--) {
221 | existingNodes[existingNodes.length - 1].remove();
222 | }
223 | }
224 |
225 | /**
226 | * Remove scripts from HTML
227 | * @param {Node} elem The element to remove scripts from
228 | */
229 | function removeScripts (elem) {
230 | let scripts = elem.querySelectorAll('script');
231 | for (let script of scripts) {
232 | script.remove();
233 | }
234 | }
235 |
236 | /**
237 | * Diff the existing DOM node versus the template
238 | * @param {Array} template The template HTML
239 | * @param {Node} existing The current DOM HTML
240 | */
241 | function diff (template, existing) {
242 |
243 | // Get the nodes in the template and existing UI
244 | let templateNodes = template.childNodes;
245 | let existingNodes = existing.childNodes;
246 |
247 | // Don't inject scripts
248 | if (removeScripts(template)) return;
249 |
250 | // Loop through each node in the template and compare it to the matching element in the UI
251 | templateNodes.forEach(function (node, index) {
252 |
253 | // If element doesn't exist, create it
254 | if (!existingNodes[index]) {
255 | let clone = node.cloneNode(true);
256 | addDefaultAtts(clone);
257 | existing.append(clone);
258 | return;
259 | }
260 |
261 | // If there is, but it's not the same node type, insert the new node before the existing one
262 | if (isDifferentNode(node, existingNodes[index])) {
263 |
264 | // Check if node exists further in the tree
265 | let ahead = aheadInTree(node, existingNodes, index);
266 |
267 | // If not, insert the node before the current one
268 | if (!ahead) {
269 | let clone = node.cloneNode(true);
270 | addDefaultAtts(clone);
271 | existingNodes[index].before(clone);
272 | return;
273 | }
274 |
275 | // Otherwise, move it to the current spot
276 | existingNodes[index].before(ahead);
277 |
278 | }
279 |
280 | // If attributes are different, update them
281 | diffAttributes(node, existingNodes[index]);
282 |
283 | // Stop diffing if a native web component
284 | if (node.nodeName.includes('-')) return;
285 |
286 | // If content is different, update it
287 | let templateContent = getNodeContent(node);
288 | if (templateContent && templateContent !== getNodeContent(existingNodes[index])) {
289 | existingNodes[index].textContent = templateContent;
290 | }
291 |
292 | // If there shouldn't be child nodes but there are, remove them
293 | if (!node.childNodes.length && existingNodes[index].childNodes.length) {
294 | existingNodes[index].innerHTML = '';
295 | return;
296 | }
297 |
298 | // If DOM is empty and shouldn't be, build it up
299 | // This uses a document fragment to minimize reflows
300 | if (!existingNodes[index].childNodes.length && node.childNodes.length) {
301 | let fragment = document.createDocumentFragment();
302 | diff(node, fragment);
303 | existingNodes[index].appendChild(fragment);
304 | return;
305 | }
306 |
307 | // If there are nodes within it, recursively diff those
308 | if (node.childNodes.length) {
309 | diff(node, existingNodes[index]);
310 | }
311 |
312 | });
313 |
314 | // If extra elements in DOM, remove them
315 | trimExtraNodes(existingNodes, templateNodes);
316 |
317 | }
318 |
319 | /**
320 | * Render a template into the UI
321 | * @param {Node|String} elem The element or selector to render the template into
322 | * @param {String} template The template to render
323 | */
324 | function render (elem, template) {
325 | let node = elem === 'string' ? document.querySelector(elem) : elem;
326 | let html = stringToHTML(template);
327 | diff(html, node);
328 | }
329 |
330 | return render;
331 |
332 | })();
--------------------------------------------------------------------------------
/06 - project/store.js:
--------------------------------------------------------------------------------
1 | let store = (function () {
2 |
3 | /**
4 | * Emit a custom event
5 | * @param {*} data The store data
6 | */
7 | function emit (data) {
8 |
9 | // Create a new event
10 | let event = new CustomEvent('store', {
11 | bubbles: true,
12 | cancelable: true,
13 | detail: data
14 | });
15 |
16 | // Dispatch the event
17 | return document.dispatchEvent(event);
18 |
19 | }
20 |
21 | /**
22 | * Create a Proxy handler object
23 | * @param {Object} data The data object
24 | * @return {Object} The handler object
25 | */
26 | function handler (data) {
27 | return {
28 | get (obj, prop) {
29 | if (prop === '_isProxy') return true;
30 | if (['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(obj[prop])) && !obj[prop]._isProxy) {
31 | obj[prop] = new Proxy(obj[prop], handler(data));
32 | }
33 | return obj[prop];
34 | },
35 | set (obj, prop, value) {
36 | if (obj[prop] === value) return true;
37 | obj[prop] = value;
38 | emit(data);
39 | return true;
40 | },
41 | deleteProperty (obj, prop) {
42 | delete obj[prop];
43 | emit(data);
44 | return true;
45 | }
46 | };
47 | }
48 |
49 | /**
50 | * Create a new store
51 | * @param {Object} data The data object
52 | * @return {Proxy} The reactive proxy
53 | */
54 | function store (data = {}) {
55 | data = ['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(data)) ? data : {value: data};
56 | return new Proxy(data, handler(data));
57 | }
58 |
59 | return store;
60 |
61 | })();
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The State-Based UI Pocket Guide Source Code
2 | All of the source code for the [State-Based UI Pocket Guide](https://vanillajsguides.com).
--------------------------------------------------------------------------------