├── .gitignore ├── favicon.ico ├── index.html ├── lib └── demo-step-by-step │ ├── dom.1.js │ ├── dom.10.js │ ├── dom.11.js │ ├── dom.12.js │ ├── dom.13.js │ ├── dom.14.js │ ├── dom.15.js │ ├── dom.16.js │ ├── dom.17.js │ ├── dom.18.js │ ├── dom.19.js │ ├── dom.2.js │ ├── dom.3.js │ ├── dom.4.js │ ├── dom.5.js │ ├── dom.6.js │ ├── dom.7.js │ ├── dom.8.js │ ├── dom.9.js │ └── dom.js ├── package-lock.json ├── package.json ├── scripts └── babel.min.js ├── server.js ├── style ├── app.css ├── fontawesome │ └── all.css └── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.svg │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.eot │ ├── fa-regular-400.svg │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.eot │ ├── fa-solid-900.svg │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ └── fa-solid-900.woff2 ├── todo-app.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # editor 7 | .vscode 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Code your own ReactJS 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.1.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | 3 | const TinyReact = (function () { 4 | function createElement(type, attributes = {}, ...children) { 5 | 6 | } 7 | 8 | return { 9 | createElement 10 | } 11 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.10.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component (todo) 11 | 12 | const TinyReact = (function () { 13 | function createElement(type, attributes = {}, ...children) { 14 | let childElements = [].concat(...children).reduce( 15 | (acc, child) => { 16 | if (child != null && child !== true && child !== false) { 17 | if (child instanceof Object) { 18 | acc.push(child); 19 | } else { 20 | acc.push(createElement("text", { 21 | textContent: child 22 | })); 23 | } 24 | } 25 | return acc; 26 | } 27 | , []); 28 | return { 29 | type, 30 | children: childElements, 31 | props: Object.assign({ children: childElements }, attributes) 32 | } 33 | } 34 | 35 | const render = function (vdom, container, oldDom = container.firstChild) { 36 | diff(vdom, container, oldDom); 37 | } 38 | 39 | const diff = function (vdom, container, oldDom) { 40 | let oldvdom = oldDom && oldDom._virtualElement; 41 | 42 | if (!oldDom) { 43 | mountElement(vdom, container, oldDom); 44 | } 45 | else if (typeof vdom.type === "function") { 46 | diffComponent(vdom, null, container, oldDom); 47 | } 48 | else if (oldvdom && oldvdom.type === vdom.type) { 49 | if (oldvdom.type === "text") { 50 | updateTextNode(oldDom, vdom, oldvdom); 51 | } else { 52 | updateDomElement(oldDom, vdom, oldvdom); 53 | } 54 | 55 | // Set a reference to updated vdom 56 | oldDom._virtualElement = vdom; 57 | 58 | // Recursively diff children.. 59 | // Doing an index by index diffing (because we don't have keys yet) 60 | vdom.children.forEach((child, i) => { 61 | diff(child, oldDom, oldDom.childNodes[i]); 62 | }); 63 | 64 | // Remove old dom nodes 65 | let oldNodes = oldDom.childNodes; 66 | if (oldNodes.length > vdom.children.length) { 67 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 68 | let nodeToBeRemoved = oldNodes[i]; 69 | unmountNode(nodeToBeRemoved, oldDom); 70 | } 71 | } 72 | 73 | } 74 | } 75 | 76 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 77 | if (!oldComponent) { 78 | mountElement(newVirtualElement, container, domElement); 79 | } 80 | } 81 | 82 | const mountElement = function (vdom, container, oldDom) { 83 | if (isFunction(vdom)) { 84 | return mountComponent(vdom, container, oldDom); 85 | } else { 86 | return mountSimpleNode(vdom, container, oldDom); 87 | } 88 | } 89 | 90 | function isFunction(obj) { 91 | return obj && 'function' === typeof obj.type; 92 | } 93 | 94 | function isFunctionalComponent(vnode) { 95 | let nodeType = vnode && vnode.type; 96 | return nodeType && isFunction(vnode) 97 | && !(nodeType.prototype && nodeType.prototype.render); 98 | } 99 | 100 | function buildFunctionalComponent(vnode, context) { 101 | return vnode.type(vnode.props || {}); 102 | } 103 | 104 | 105 | function mountComponent(vdom, container, oldDomElement) { 106 | let nextvDom = null, component = null, newDomElement = null; 107 | if (isFunctionalComponent(vdom)) { 108 | nextvDom = buildFunctionalComponent(vdom); 109 | } 110 | 111 | // Recursively render child components 112 | if (isFunction(nextvDom)) { 113 | return mountComponent(nextvDom, container, oldDomElement); 114 | } else { 115 | newDomElement = mountElement(nextvDom, container, oldDomElement); 116 | } 117 | return newDomElement; 118 | 119 | } 120 | 121 | function unmountNode(domElement, parentComponent) { 122 | domElement.remove(); 123 | } 124 | 125 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 126 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 127 | domElement.textContent = newVirtualElement.props.textContent; 128 | } 129 | // Set a reference to the newvddom in oldDom 130 | domElement._virtualElement = newVirtualElement; 131 | } 132 | 133 | 134 | 135 | 136 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 137 | let newDomElement = null; 138 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 139 | 140 | if (vdom.type === "text") { 141 | newDomElement = document.createTextNode(vdom.props.textContent); 142 | } else { 143 | newDomElement = document.createElement(vdom.type); 144 | updateDomElement(newDomElement, vdom); 145 | } 146 | 147 | // Setting reference to vdom to dom 148 | newDomElement._virtualElement = vdom; 149 | if (nextSibling) { 150 | container.insertBefore(newDomElement, nextSibling); 151 | } else { 152 | container.appendChild(newDomElement); 153 | } 154 | 155 | vdom.children.forEach(child => { 156 | mountElement(child, newDomElement); 157 | }); 158 | 159 | } 160 | 161 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 162 | const newProps = newVirtualElement.props || {}; 163 | const oldProps = oldVirtualElement.props || {}; 164 | Object.keys(newProps).forEach(propName => { 165 | const newProp = newProps[propName]; 166 | const oldProp = oldProps[propName]; 167 | if (newProp !== oldProp) { 168 | if (propName.slice(0, 2) === "on") { 169 | // prop is an event handler 170 | const eventName = propName.toLowerCase().slice(2); 171 | domElement.addEventListener(eventName, newProp, false); 172 | if (oldProp) { 173 | domElement.removeEventListener(eventName, oldProp, false); 174 | } 175 | } else if (propName === "value" || propName === "checked") { 176 | // this are special attributes that cannot be set 177 | // using setAttribute 178 | domElement[propName] = newProp; 179 | } else if (propName !== "children") { 180 | // ignore the 'children' prop 181 | if (propName === "className") { 182 | domElement.setAttribute("class", newProps[propName]); 183 | } else { 184 | domElement.setAttribute(propName, newProps[propName]); 185 | } 186 | } 187 | } 188 | }); 189 | // remove oldProps 190 | Object.keys(oldProps).forEach(propName => { 191 | const newProp = newProps[propName]; 192 | const oldProp = oldProps[propName]; 193 | if (!newProp) { 194 | if (propName.slice(0, 2) === "on") { 195 | // prop is an event handler 196 | domElement.removeEventListener(propName, oldProp, false); 197 | } else if (propName !== "children") { 198 | // ignore the 'children' prop 199 | domElement.removeAttribute(propName); 200 | } 201 | } 202 | }); 203 | } 204 | 205 | 206 | return { 207 | createElement, 208 | render 209 | } 210 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.11.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component (todo) 11 | // 11. functional component diffing (remove extra nodes) 12 | 13 | const TinyReact = (function () { 14 | function createElement(type, attributes = {}, ...children) { 15 | let childElements = [].concat(...children).reduce( 16 | (acc, child) => { 17 | if (child != null && child !== true && child !== false) { 18 | if (child instanceof Object) { 19 | acc.push(child); 20 | } else { 21 | acc.push(createElement("text", { 22 | textContent: child 23 | })); 24 | } 25 | } 26 | return acc; 27 | } 28 | , []); 29 | return { 30 | type, 31 | children: childElements, 32 | props: Object.assign({ children: childElements }, attributes) 33 | } 34 | } 35 | 36 | const render = function (vdom, container, oldDom = container.firstChild) { 37 | diff(vdom, container, oldDom); 38 | } 39 | 40 | const diff = function (vdom, container, oldDom) { 41 | let oldvdom = oldDom && oldDom._virtualElement; 42 | 43 | if (!oldDom) { 44 | mountElement(vdom, container, oldDom); 45 | } 46 | else if (typeof vdom.type === "function") { 47 | diffComponent(vdom, null, container, oldDom); 48 | } 49 | else if (oldvdom && oldvdom.type === vdom.type) { 50 | if (oldvdom.type === "text") { 51 | updateTextNode(oldDom, vdom, oldvdom); 52 | } else { 53 | updateDomElement(oldDom, vdom, oldvdom); 54 | } 55 | 56 | // Set a reference to updated vdom 57 | oldDom._virtualElement = vdom; 58 | 59 | // Recursively diff children.. 60 | // Doing an index by index diffing (because we don't have keys yet) 61 | vdom.children.forEach((child, i) => { 62 | diff(child, oldDom, oldDom.childNodes[i]); 63 | }); 64 | 65 | // Remove old dom nodes 66 | let oldNodes = oldDom.childNodes; 67 | if (oldNodes.length > vdom.children.length) { 68 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 69 | let nodeToBeRemoved = oldNodes[i]; 70 | unmountNode(nodeToBeRemoved, oldDom); 71 | } 72 | } 73 | 74 | } 75 | } 76 | 77 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 78 | 79 | if (!oldComponent) { 80 | mountElement(newVirtualElement, container, domElement); 81 | } 82 | } 83 | 84 | const mountElement = function (vdom, container, oldDom) { 85 | if (isFunction(vdom)) { 86 | return mountComponent(vdom, container, oldDom); 87 | } else { 88 | return mountSimpleNode(vdom, container, oldDom); 89 | } 90 | } 91 | 92 | function isFunction(obj) { 93 | return obj && 'function' === typeof obj.type; 94 | } 95 | 96 | function isFunctionalComponent(vnode) { 97 | let nodeType = vnode && vnode.type; 98 | return nodeType && isFunction(vnode) 99 | && !(nodeType.prototype && nodeType.prototype.render); 100 | } 101 | 102 | function buildFunctionalComponent(vnode, context) { 103 | return vnode.type(vnode.props || {}); 104 | } 105 | 106 | 107 | function mountComponent(vdom, container, oldDomElement) { 108 | let nextvDom = null, component = null, newDomElement = null; 109 | if (isFunctionalComponent(vdom)) { 110 | nextvDom = buildFunctionalComponent(vdom); 111 | } 112 | 113 | // Recursively render child components 114 | if (isFunction(nextvDom)) { 115 | return mountComponent(nextvDom, container, oldDomElement); 116 | } else { 117 | newDomElement = mountElement(nextvDom, container, oldDomElement); 118 | } 119 | return newDomElement; 120 | 121 | } 122 | 123 | function unmountNode(domElement, parentComponent) { 124 | domElement.remove(); 125 | } 126 | 127 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 128 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 129 | domElement.textContent = newVirtualElement.props.textContent; 130 | } 131 | // Set a reference to the newvddom in oldDom 132 | domElement._virtualElement = newVirtualElement; 133 | } 134 | 135 | 136 | 137 | 138 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 139 | let newDomElement = null; 140 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 141 | 142 | if (vdom.type === "text") { 143 | newDomElement = document.createTextNode(vdom.props.textContent); 144 | } else { 145 | newDomElement = document.createElement(vdom.type); 146 | updateDomElement(newDomElement, vdom); 147 | } 148 | 149 | // Setting reference to vdom to dom 150 | newDomElement._virtualElement = vdom; 151 | 152 | // TODO: Remove old nodes 153 | if (oldDomElement) { 154 | unmountNode(oldDomElement, parentComponent); 155 | } 156 | 157 | if (nextSibling) { 158 | container.insertBefore(newDomElement, nextSibling); 159 | } else { 160 | container.appendChild(newDomElement); 161 | } 162 | 163 | vdom.children.forEach(child => { 164 | mountElement(child, newDomElement); 165 | }); 166 | 167 | } 168 | 169 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 170 | const newProps = newVirtualElement.props || {}; 171 | const oldProps = oldVirtualElement.props || {}; 172 | Object.keys(newProps).forEach(propName => { 173 | const newProp = newProps[propName]; 174 | const oldProp = oldProps[propName]; 175 | if (newProp !== oldProp) { 176 | if (propName.slice(0, 2) === "on") { 177 | // prop is an event handler 178 | const eventName = propName.toLowerCase().slice(2); 179 | domElement.addEventListener(eventName, newProp, false); 180 | if (oldProp) { 181 | domElement.removeEventListener(eventName, oldProp, false); 182 | } 183 | } else if (propName === "value" || propName === "checked") { 184 | // this are special attributes that cannot be set 185 | // using setAttribute 186 | domElement[propName] = newProp; 187 | } else if (propName !== "children") { 188 | // ignore the 'children' prop 189 | if (propName === "className") { 190 | domElement.setAttribute("class", newProps[propName]); 191 | } else { 192 | domElement.setAttribute(propName, newProps[propName]); 193 | } 194 | } 195 | } 196 | }); 197 | // remove oldProps 198 | Object.keys(oldProps).forEach(propName => { 199 | const newProp = newProps[propName]; 200 | const oldProp = oldProps[propName]; 201 | if (!newProp) { 202 | if (propName.slice(0, 2) === "on") { 203 | // prop is an event handler 204 | domElement.removeEventListener(propName, oldProp, false); 205 | } else if (propName !== "children") { 206 | // ignore the 'children' prop 207 | domElement.removeAttribute(propName); 208 | } 209 | } 210 | }); 211 | } 212 | 213 | 214 | return { 215 | createElement, 216 | render 217 | } 218 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.12.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component (todo) 13 | 14 | const TinyReact = (function () { 15 | function createElement(type, attributes = {}, ...children) { 16 | let childElements = [].concat(...children).reduce( 17 | (acc, child) => { 18 | if (child != null && child !== true && child !== false) { 19 | if (child instanceof Object) { 20 | acc.push(child); 21 | } else { 22 | acc.push(createElement("text", { 23 | textContent: child 24 | })); 25 | } 26 | } 27 | return acc; 28 | } 29 | , []); 30 | return { 31 | type, 32 | children: childElements, 33 | props: Object.assign({ children: childElements }, attributes) 34 | } 35 | } 36 | 37 | const render = function (vdom, container, oldDom = container.firstChild) { 38 | diff(vdom, container, oldDom); 39 | } 40 | 41 | const diff = function (vdom, container, oldDom) { 42 | let oldvdom = oldDom && oldDom._virtualElement; 43 | 44 | if (!oldDom) { 45 | mountElement(vdom, container, oldDom); 46 | } 47 | else if (typeof vdom.type === "function") { 48 | diffComponent(vdom, null, container, oldDom); 49 | } 50 | else if (oldvdom && oldvdom.type === vdom.type) { 51 | if (oldvdom.type === "text") { 52 | updateTextNode(oldDom, vdom, oldvdom); 53 | } else { 54 | updateDomElement(oldDom, vdom, oldvdom); 55 | } 56 | 57 | // Set a reference to updated vdom 58 | oldDom._virtualElement = vdom; 59 | 60 | // Recursively diff children.. 61 | // Doing an index by index diffing (because we don't have keys yet) 62 | vdom.children.forEach((child, i) => { 63 | diff(child, oldDom, oldDom.childNodes[i]); 64 | }); 65 | 66 | // Remove old dom nodes 67 | let oldNodes = oldDom.childNodes; 68 | if (oldNodes.length > vdom.children.length) { 69 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 70 | let nodeToBeRemoved = oldNodes[i]; 71 | unmountNode(nodeToBeRemoved, oldDom); 72 | } 73 | } 74 | 75 | } 76 | } 77 | 78 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 79 | 80 | if (!oldComponent) { 81 | mountElement(newVirtualElement, container, domElement); 82 | } 83 | } 84 | 85 | const mountElement = function (vdom, container, oldDom) { 86 | if (isFunction(vdom)) { 87 | return mountComponent(vdom, container, oldDom); 88 | } else { 89 | return mountSimpleNode(vdom, container, oldDom); 90 | } 91 | } 92 | 93 | function isFunction(obj) { 94 | return obj && 'function' === typeof obj.type; 95 | } 96 | 97 | function isFunctionalComponent(vnode) { 98 | let nodeType = vnode && vnode.type; 99 | return nodeType && isFunction(vnode) 100 | && !(nodeType.prototype && nodeType.prototype.render); 101 | } 102 | 103 | function buildFunctionalComponent(vnode, context) { 104 | return vnode.type(vnode.props || {}); 105 | } 106 | 107 | 108 | function buildStatefulComponent(virtualElement) { 109 | const component = new virtualElement.type(); 110 | const nextElement = component.render(); 111 | nextElement.component = component; 112 | return nextElement; 113 | } 114 | 115 | function mountComponent(vdom, container, oldDomElement) { 116 | let nextvDom = null, component = null, newDomElement = null; 117 | if (isFunctionalComponent(vdom)) { 118 | nextvDom = buildFunctionalComponent(vdom); 119 | } else { 120 | nextvDom = buildStatefulComponent(vdom); 121 | } 122 | 123 | // Recursively render child components 124 | if (isFunction(nextvDom)) { 125 | return mountComponent(nextvDom, container, oldDomElement); 126 | } else { 127 | newDomElement = mountElement(nextvDom, container, oldDomElement); 128 | } 129 | return newDomElement; 130 | 131 | } 132 | 133 | function unmountNode(domElement, parentComponent) { 134 | domElement.remove(); 135 | } 136 | 137 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 138 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 139 | domElement.textContent = newVirtualElement.props.textContent; 140 | } 141 | // Set a reference to the newvddom in oldDom 142 | domElement._virtualElement = newVirtualElement; 143 | } 144 | 145 | 146 | 147 | 148 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 149 | let newDomElement = null; 150 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 151 | 152 | if (vdom.type === "text") { 153 | newDomElement = document.createTextNode(vdom.props.textContent); 154 | } else { 155 | newDomElement = document.createElement(vdom.type); 156 | updateDomElement(newDomElement, vdom); 157 | } 158 | 159 | // Setting reference to vdom to dom 160 | newDomElement._virtualElement = vdom; 161 | 162 | // TODO: Remove old nodes 163 | if (oldDomElement) { 164 | unmountNode(oldDomElement, parentComponent); 165 | } 166 | 167 | if (nextSibling) { 168 | container.insertBefore(newDomElement, nextSibling); 169 | } else { 170 | container.appendChild(newDomElement); 171 | } 172 | 173 | vdom.children.forEach(child => { 174 | mountElement(child, newDomElement); 175 | }); 176 | 177 | } 178 | 179 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 180 | const newProps = newVirtualElement.props || {}; 181 | const oldProps = oldVirtualElement.props || {}; 182 | Object.keys(newProps).forEach(propName => { 183 | const newProp = newProps[propName]; 184 | const oldProp = oldProps[propName]; 185 | if (newProp !== oldProp) { 186 | if (propName.slice(0, 2) === "on") { 187 | // prop is an event handler 188 | const eventName = propName.toLowerCase().slice(2); 189 | domElement.addEventListener(eventName, newProp, false); 190 | if (oldProp) { 191 | domElement.removeEventListener(eventName, oldProp, false); 192 | } 193 | } else if (propName === "value" || propName === "checked") { 194 | // this are special attributes that cannot be set 195 | // using setAttribute 196 | domElement[propName] = newProp; 197 | } else if (propName !== "children") { 198 | // ignore the 'children' prop 199 | if (propName === "className") { 200 | domElement.setAttribute("class", newProps[propName]); 201 | } else { 202 | domElement.setAttribute(propName, newProps[propName]); 203 | } 204 | } 205 | } 206 | }); 207 | // remove oldProps 208 | Object.keys(oldProps).forEach(propName => { 209 | const newProp = newProps[propName]; 210 | const oldProp = oldProps[propName]; 211 | if (!newProp) { 212 | if (propName.slice(0, 2) === "on") { 213 | // prop is an event handler 214 | domElement.removeEventListener(propName, oldProp, false); 215 | } else if (propName !== "children") { 216 | // ignore the 'children' prop 217 | domElement.removeAttribute(propName); 218 | } 219 | } 220 | }); 221 | } 222 | 223 | class Component { 224 | constructor(props) { 225 | 226 | } 227 | } 228 | 229 | 230 | return { 231 | createElement, 232 | render, 233 | Component 234 | } 235 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.13.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component 13 | // 13. passing props to stateful component 14 | 15 | const TinyReact = (function () { 16 | function createElement(type, attributes = {}, ...children) { 17 | let childElements = [].concat(...children).reduce( 18 | (acc, child) => { 19 | if (child != null && child !== true && child !== false) { 20 | if (child instanceof Object) { 21 | acc.push(child); 22 | } else { 23 | acc.push(createElement("text", { 24 | textContent: child 25 | })); 26 | } 27 | } 28 | return acc; 29 | } 30 | , []); 31 | return { 32 | type, 33 | children: childElements, 34 | props: Object.assign({ children: childElements }, attributes) 35 | } 36 | } 37 | 38 | const render = function (vdom, container, oldDom = container.firstChild) { 39 | diff(vdom, container, oldDom); 40 | } 41 | 42 | const diff = function (vdom, container, oldDom) { 43 | let oldvdom = oldDom && oldDom._virtualElement; 44 | 45 | if (!oldDom) { 46 | mountElement(vdom, container, oldDom); 47 | } 48 | else if (typeof vdom.type === "function") { 49 | diffComponent(vdom, null, container, oldDom); 50 | } 51 | else if (oldvdom && oldvdom.type === vdom.type) { 52 | if (oldvdom.type === "text") { 53 | updateTextNode(oldDom, vdom, oldvdom); 54 | } else { 55 | updateDomElement(oldDom, vdom, oldvdom); 56 | } 57 | 58 | // Set a reference to updated vdom 59 | oldDom._virtualElement = vdom; 60 | 61 | // Recursively diff children.. 62 | // Doing an index by index diffing (because we don't have keys yet) 63 | vdom.children.forEach((child, i) => { 64 | diff(child, oldDom, oldDom.childNodes[i]); 65 | }); 66 | 67 | // Remove old dom nodes 68 | let oldNodes = oldDom.childNodes; 69 | if (oldNodes.length > vdom.children.length) { 70 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 71 | let nodeToBeRemoved = oldNodes[i]; 72 | unmountNode(nodeToBeRemoved, oldDom); 73 | } 74 | } 75 | 76 | } 77 | } 78 | 79 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 80 | 81 | if (!oldComponent) { 82 | mountElement(newVirtualElement, container, domElement); 83 | } 84 | } 85 | 86 | const mountElement = function (vdom, container, oldDom) { 87 | if (isFunction(vdom)) { 88 | return mountComponent(vdom, container, oldDom); 89 | } else { 90 | return mountSimpleNode(vdom, container, oldDom); 91 | } 92 | } 93 | 94 | function isFunction(obj) { 95 | return obj && 'function' === typeof obj.type; 96 | } 97 | 98 | function isFunctionalComponent(vnode) { 99 | let nodeType = vnode && vnode.type; 100 | return nodeType && isFunction(vnode) 101 | && !(nodeType.prototype && nodeType.prototype.render); 102 | } 103 | 104 | function buildFunctionalComponent(vnode, context) { 105 | return vnode.type(vnode.props || {}); 106 | } 107 | 108 | 109 | function buildStatefulComponent(virtualElement) { 110 | const component = new virtualElement.type(virtualElement.props); 111 | const nextElement = component.render(); 112 | nextElement.component = component; 113 | return nextElement; 114 | } 115 | 116 | function mountComponent(vdom, container, oldDomElement) { 117 | let nextvDom = null, component = null, newDomElement = null; 118 | if (isFunctionalComponent(vdom)) { 119 | nextvDom = buildFunctionalComponent(vdom); 120 | } else { 121 | nextvDom = buildStatefulComponent(vdom); 122 | } 123 | 124 | // Recursively render child components 125 | if (isFunction(nextvDom)) { 126 | return mountComponent(nextvDom, container, oldDomElement); 127 | } else { 128 | newDomElement = mountElement(nextvDom, container, oldDomElement); 129 | } 130 | return newDomElement; 131 | 132 | } 133 | 134 | function unmountNode(domElement, parentComponent) { 135 | domElement.remove(); 136 | } 137 | 138 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 139 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 140 | domElement.textContent = newVirtualElement.props.textContent; 141 | } 142 | // Set a reference to the newvddom in oldDom 143 | domElement._virtualElement = newVirtualElement; 144 | } 145 | 146 | 147 | 148 | 149 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 150 | let newDomElement = null; 151 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 152 | 153 | if (vdom.type === "text") { 154 | newDomElement = document.createTextNode(vdom.props.textContent); 155 | } else { 156 | newDomElement = document.createElement(vdom.type); 157 | updateDomElement(newDomElement, vdom); 158 | } 159 | 160 | // Setting reference to vdom to dom 161 | newDomElement._virtualElement = vdom; 162 | 163 | // TODO: Remove old nodes 164 | if (oldDomElement) { 165 | unmountNode(oldDomElement, parentComponent); 166 | } 167 | 168 | if (nextSibling) { 169 | container.insertBefore(newDomElement, nextSibling); 170 | } else { 171 | container.appendChild(newDomElement); 172 | } 173 | 174 | vdom.children.forEach(child => { 175 | mountElement(child, newDomElement); 176 | }); 177 | 178 | } 179 | 180 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 181 | const newProps = newVirtualElement.props || {}; 182 | const oldProps = oldVirtualElement.props || {}; 183 | Object.keys(newProps).forEach(propName => { 184 | const newProp = newProps[propName]; 185 | const oldProp = oldProps[propName]; 186 | if (newProp !== oldProp) { 187 | if (propName.slice(0, 2) === "on") { 188 | // prop is an event handler 189 | const eventName = propName.toLowerCase().slice(2); 190 | domElement.addEventListener(eventName, newProp, false); 191 | if (oldProp) { 192 | domElement.removeEventListener(eventName, oldProp, false); 193 | } 194 | } else if (propName === "value" || propName === "checked") { 195 | // this are special attributes that cannot be set 196 | // using setAttribute 197 | domElement[propName] = newProp; 198 | } else if (propName !== "children") { 199 | // ignore the 'children' prop 200 | if (propName === "className") { 201 | domElement.setAttribute("class", newProps[propName]); 202 | } else { 203 | domElement.setAttribute(propName, newProps[propName]); 204 | } 205 | } 206 | } 207 | }); 208 | // remove oldProps 209 | Object.keys(oldProps).forEach(propName => { 210 | const newProp = newProps[propName]; 211 | const oldProp = oldProps[propName]; 212 | if (!newProp) { 213 | if (propName.slice(0, 2) === "on") { 214 | // prop is an event handler 215 | domElement.removeEventListener(propName, oldProp, false); 216 | } else if (propName !== "children") { 217 | // ignore the 'children' prop 218 | domElement.removeAttribute(propName); 219 | } 220 | } 221 | }); 222 | } 223 | 224 | class Component { 225 | constructor(props) { 226 | this.props = props; 227 | } 228 | } 229 | 230 | 231 | return { 232 | createElement, 233 | render, 234 | Component 235 | } 236 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.14.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component 13 | // 13. passing props to stateful component 14 | // 14. implement setState() method 15 | 16 | 17 | const TinyReact = (function () { 18 | function createElement(type, attributes = {}, ...children) { 19 | let childElements = [].concat(...children).reduce( 20 | (acc, child) => { 21 | if (child != null && child !== true && child !== false) { 22 | if (child instanceof Object) { 23 | acc.push(child); 24 | } else { 25 | acc.push(createElement("text", { 26 | textContent: child 27 | })); 28 | } 29 | } 30 | return acc; 31 | } 32 | , []); 33 | return { 34 | type, 35 | children: childElements, 36 | props: Object.assign({ children: childElements }, attributes) 37 | } 38 | } 39 | 40 | const render = function (vdom, container, oldDom = container.firstChild) { 41 | diff(vdom, container, oldDom); 42 | } 43 | 44 | const diff = function (vdom, container, oldDom) { 45 | let oldvdom = oldDom && oldDom._virtualElement; 46 | 47 | if (!oldDom) { 48 | mountElement(vdom, container, oldDom); 49 | } 50 | else if (typeof vdom.type === "function") { 51 | diffComponent(vdom, null, container, oldDom); 52 | } 53 | else if (oldvdom && oldvdom.type === vdom.type) { 54 | if (oldvdom.type === "text") { 55 | updateTextNode(oldDom, vdom, oldvdom); 56 | } else { 57 | updateDomElement(oldDom, vdom, oldvdom); 58 | } 59 | 60 | // Set a reference to updated vdom 61 | oldDom._virtualElement = vdom; 62 | 63 | // Recursively diff children.. 64 | // Doing an index by index diffing (because we don't have keys yet) 65 | vdom.children.forEach((child, i) => { 66 | diff(child, oldDom, oldDom.childNodes[i]); 67 | }); 68 | 69 | // Remove old dom nodes 70 | let oldNodes = oldDom.childNodes; 71 | if (oldNodes.length > vdom.children.length) { 72 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 73 | let nodeToBeRemoved = oldNodes[i]; 74 | unmountNode(nodeToBeRemoved, oldDom); 75 | } 76 | } 77 | 78 | } 79 | } 80 | 81 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 82 | 83 | if (!oldComponent) { 84 | mountElement(newVirtualElement, container, domElement); 85 | } 86 | } 87 | 88 | const mountElement = function (vdom, container, oldDom) { 89 | if (isFunction(vdom)) { 90 | return mountComponent(vdom, container, oldDom); 91 | } else { 92 | return mountSimpleNode(vdom, container, oldDom); 93 | } 94 | } 95 | 96 | function isFunction(obj) { 97 | return obj && 'function' === typeof obj.type; 98 | } 99 | 100 | function isFunctionalComponent(vnode) { 101 | let nodeType = vnode && vnode.type; 102 | return nodeType && isFunction(vnode) 103 | && !(nodeType.prototype && nodeType.prototype.render); 104 | } 105 | 106 | function buildFunctionalComponent(vnode, context) { 107 | return vnode.type(vnode.props || {}); 108 | } 109 | 110 | 111 | function buildStatefulComponent(virtualElement) { 112 | const component = new virtualElement.type(virtualElement.props); 113 | const nextElement = component.render(); 114 | nextElement.component = component; 115 | return nextElement; 116 | } 117 | 118 | function mountComponent(vdom, container, oldDomElement) { 119 | let nextvDom = null, component = null, newDomElement = null; 120 | if (isFunctionalComponent(vdom)) { 121 | nextvDom = buildFunctionalComponent(vdom); 122 | } else { 123 | nextvDom = buildStatefulComponent(vdom); 124 | } 125 | 126 | // Recursively render child components 127 | if (isFunction(nextvDom)) { 128 | return mountComponent(nextvDom, container, oldDomElement); 129 | } else { 130 | newDomElement = mountElement(nextvDom, container, oldDomElement); 131 | } 132 | return newDomElement; 133 | 134 | } 135 | 136 | function unmountNode(domElement, parentComponent) { 137 | domElement.remove(); 138 | } 139 | 140 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 141 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 142 | domElement.textContent = newVirtualElement.props.textContent; 143 | } 144 | // Set a reference to the newvddom in oldDom 145 | domElement._virtualElement = newVirtualElement; 146 | } 147 | 148 | 149 | 150 | 151 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 152 | let newDomElement = null; 153 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 154 | 155 | if (vdom.type === "text") { 156 | newDomElement = document.createTextNode(vdom.props.textContent); 157 | } else { 158 | newDomElement = document.createElement(vdom.type); 159 | updateDomElement(newDomElement, vdom); 160 | } 161 | 162 | // Setting reference to vdom to dom 163 | newDomElement._virtualElement = vdom; 164 | 165 | // TODO: Remove old nodes 166 | if (oldDomElement) { 167 | unmountNode(oldDomElement, parentComponent); 168 | } 169 | 170 | if (nextSibling) { 171 | container.insertBefore(newDomElement, nextSibling); 172 | } else { 173 | container.appendChild(newDomElement); 174 | } 175 | 176 | let component = vdom.component; 177 | if (component) { 178 | component.setDomElement(newDomElement); 179 | } 180 | 181 | vdom.children.forEach(child => { 182 | mountElement(child, newDomElement); 183 | }); 184 | 185 | } 186 | 187 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 188 | const newProps = newVirtualElement.props || {}; 189 | const oldProps = oldVirtualElement.props || {}; 190 | Object.keys(newProps).forEach(propName => { 191 | const newProp = newProps[propName]; 192 | const oldProp = oldProps[propName]; 193 | if (newProp !== oldProp) { 194 | if (propName.slice(0, 2) === "on") { 195 | // prop is an event handler 196 | const eventName = propName.toLowerCase().slice(2); 197 | domElement.addEventListener(eventName, newProp, false); 198 | if (oldProp) { 199 | domElement.removeEventListener(eventName, oldProp, false); 200 | } 201 | } else if (propName === "value" || propName === "checked") { 202 | // this are special attributes that cannot be set 203 | // using setAttribute 204 | domElement[propName] = newProp; 205 | } else if (propName !== "children") { 206 | // ignore the 'children' prop 207 | if (propName === "className") { 208 | domElement.setAttribute("class", newProps[propName]); 209 | } else { 210 | domElement.setAttribute(propName, newProps[propName]); 211 | } 212 | } 213 | } 214 | }); 215 | // remove oldProps 216 | Object.keys(oldProps).forEach(propName => { 217 | const newProp = newProps[propName]; 218 | const oldProp = oldProps[propName]; 219 | if (!newProp) { 220 | if (propName.slice(0, 2) === "on") { 221 | // prop is an event handler 222 | domElement.removeEventListener(propName, oldProp, false); 223 | } else if (propName !== "children") { 224 | // ignore the 'children' prop 225 | domElement.removeAttribute(propName); 226 | } 227 | } 228 | }); 229 | } 230 | 231 | class Component { 232 | constructor(props) { 233 | this.props = props; 234 | this.state = {}; 235 | this.prevState = {}; 236 | } 237 | 238 | setState(nextState) { 239 | if (!this.prevState) this.prevState = this.state; 240 | 241 | this.state = Object.assign({}, this.state, nextState); 242 | 243 | let dom = this.getDomElement(); 244 | let container = dom.parentNode; 245 | 246 | let newvdom = this.render(); 247 | 248 | // Recursively diff 249 | diff(newvdom, container, dom); 250 | 251 | } 252 | 253 | // Helper methods 254 | setDomElement(dom) { 255 | this._dom = dom; 256 | } 257 | 258 | getDomElement() { 259 | return this._dom; 260 | } 261 | } 262 | 263 | 264 | return { 265 | createElement, 266 | render, 267 | Component 268 | } 269 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.15.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component 13 | // 13. passing props to stateful component 14 | // 14. implement setState() method -> parameter as object 15 | // 15. implement the stub for lifecycle method (TODO) 16 | 17 | 18 | const TinyReact = (function () { 19 | function createElement(type, attributes = {}, ...children) { 20 | let childElements = [].concat(...children).reduce( 21 | (acc, child) => { 22 | if (child != null && child !== true && child !== false) { 23 | if (child instanceof Object) { 24 | acc.push(child); 25 | } else { 26 | acc.push(createElement("text", { 27 | textContent: child 28 | })); 29 | } 30 | } 31 | return acc; 32 | } 33 | , []); 34 | return { 35 | type, 36 | children: childElements, 37 | props: Object.assign({ children: childElements }, attributes) 38 | } 39 | } 40 | 41 | const render = function (vdom, container, oldDom = container.firstChild) { 42 | diff(vdom, container, oldDom); 43 | } 44 | 45 | const diff = function (vdom, container, oldDom) { 46 | let oldvdom = oldDom && oldDom._virtualElement; 47 | 48 | if (!oldDom) { 49 | mountElement(vdom, container, oldDom); 50 | } 51 | else if (typeof vdom.type === "function") { 52 | diffComponent(vdom, null, container, oldDom); 53 | } 54 | else if (oldvdom && oldvdom.type === vdom.type) { 55 | if (oldvdom.type === "text") { 56 | updateTextNode(oldDom, vdom, oldvdom); 57 | } else { 58 | updateDomElement(oldDom, vdom, oldvdom); 59 | } 60 | 61 | // Set a reference to updated vdom 62 | oldDom._virtualElement = vdom; 63 | 64 | // Recursively diff children.. 65 | // Doing an index by index diffing (because we don't have keys yet) 66 | vdom.children.forEach((child, i) => { 67 | diff(child, oldDom, oldDom.childNodes[i]); 68 | }); 69 | 70 | // Remove old dom nodes 71 | let oldNodes = oldDom.childNodes; 72 | if (oldNodes.length > vdom.children.length) { 73 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 74 | let nodeToBeRemoved = oldNodes[i]; 75 | unmountNode(nodeToBeRemoved, oldDom); 76 | } 77 | } 78 | 79 | } 80 | } 81 | 82 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 83 | 84 | if (!oldComponent) { 85 | mountElement(newVirtualElement, container, domElement); 86 | } 87 | } 88 | 89 | const mountElement = function (vdom, container, oldDom) { 90 | if (isFunction(vdom)) { 91 | return mountComponent(vdom, container, oldDom); 92 | } else { 93 | return mountSimpleNode(vdom, container, oldDom); 94 | } 95 | } 96 | 97 | function isFunction(obj) { 98 | return obj && 'function' === typeof obj.type; 99 | } 100 | 101 | function isFunctionalComponent(vnode) { 102 | let nodeType = vnode && vnode.type; 103 | return nodeType && isFunction(vnode) 104 | && !(nodeType.prototype && nodeType.prototype.render); 105 | } 106 | 107 | function buildFunctionalComponent(vnode, context) { 108 | return vnode.type(vnode.props || {}); 109 | } 110 | 111 | 112 | function buildStatefulComponent(virtualElement) { 113 | const component = new virtualElement.type(virtualElement.props); 114 | const nextElement = component.render(); 115 | nextElement.component = component; 116 | return nextElement; 117 | } 118 | 119 | function mountComponent(vdom, container, oldDomElement) { 120 | let nextvDom = null, component = null, newDomElement = null; 121 | if (isFunctionalComponent(vdom)) { 122 | nextvDom = buildFunctionalComponent(vdom); 123 | } else { 124 | nextvDom = buildStatefulComponent(vdom); 125 | } 126 | 127 | // Recursively render child components 128 | if (isFunction(nextvDom)) { 129 | return mountComponent(nextvDom, container, oldDomElement); 130 | } else { 131 | newDomElement = mountElement(nextvDom, container, oldDomElement); 132 | } 133 | return newDomElement; 134 | 135 | } 136 | 137 | function unmountNode(domElement, parentComponent) { 138 | domElement.remove(); 139 | } 140 | 141 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 142 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 143 | domElement.textContent = newVirtualElement.props.textContent; 144 | } 145 | // Set a reference to the newvddom in oldDom 146 | domElement._virtualElement = newVirtualElement; 147 | } 148 | 149 | 150 | 151 | 152 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 153 | let newDomElement = null; 154 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 155 | 156 | if (vdom.type === "text") { 157 | newDomElement = document.createTextNode(vdom.props.textContent); 158 | } else { 159 | newDomElement = document.createElement(vdom.type); 160 | updateDomElement(newDomElement, vdom); 161 | } 162 | 163 | // Setting reference to vdom to dom 164 | newDomElement._virtualElement = vdom; 165 | 166 | // TODO: Remove old nodes 167 | if (oldDomElement) { 168 | unmountNode(oldDomElement, parentComponent); 169 | } 170 | 171 | if (nextSibling) { 172 | container.insertBefore(newDomElement, nextSibling); 173 | } else { 174 | container.appendChild(newDomElement); 175 | } 176 | 177 | let component = vdom.component; 178 | if (component) { 179 | component.setDomElement(newDomElement); 180 | } 181 | 182 | vdom.children.forEach(child => { 183 | mountElement(child, newDomElement); 184 | }); 185 | 186 | } 187 | 188 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 189 | const newProps = newVirtualElement.props || {}; 190 | const oldProps = oldVirtualElement.props || {}; 191 | Object.keys(newProps).forEach(propName => { 192 | const newProp = newProps[propName]; 193 | const oldProp = oldProps[propName]; 194 | if (newProp !== oldProp) { 195 | if (propName.slice(0, 2) === "on") { 196 | // prop is an event handler 197 | const eventName = propName.toLowerCase().slice(2); 198 | domElement.addEventListener(eventName, newProp, false); 199 | if (oldProp) { 200 | domElement.removeEventListener(eventName, oldProp, false); 201 | } 202 | } else if (propName === "value" || propName === "checked") { 203 | // this are special attributes that cannot be set 204 | // using setAttribute 205 | domElement[propName] = newProp; 206 | } else if (propName !== "children") { 207 | // ignore the 'children' prop 208 | if (propName === "className") { 209 | domElement.setAttribute("class", newProps[propName]); 210 | } else { 211 | domElement.setAttribute(propName, newProps[propName]); 212 | } 213 | } 214 | } 215 | }); 216 | // remove oldProps 217 | Object.keys(oldProps).forEach(propName => { 218 | const newProp = newProps[propName]; 219 | const oldProp = oldProps[propName]; 220 | if (!newProp) { 221 | if (propName.slice(0, 2) === "on") { 222 | // prop is an event handler 223 | domElement.removeEventListener(propName, oldProp, false); 224 | } else if (propName !== "children") { 225 | // ignore the 'children' prop 226 | domElement.removeAttribute(propName); 227 | } 228 | } 229 | }); 230 | } 231 | 232 | class Component { 233 | constructor(props) { 234 | this.props = props; 235 | this.state = {}; 236 | this.prevState = {}; 237 | } 238 | 239 | setState(nextState) { 240 | if (!this.prevState) this.prevState = this.state; 241 | 242 | this.state = Object.assign({}, this.state, nextState); 243 | 244 | let dom = this.getDomElement(); 245 | let container = dom.parentNode; 246 | 247 | let newvdom = this.render(); 248 | 249 | // Recursively diff 250 | diff(newvdom, container, dom); 251 | 252 | } 253 | 254 | // Helper methods 255 | setDomElement(dom) { 256 | this._dom = dom; 257 | } 258 | 259 | getDomElement() { 260 | return this._dom; 261 | } 262 | 263 | // Lifecycle methods 264 | componentWillMount() { } 265 | componentDidMount() { } 266 | componentWillReceiveProps(nextProps) { } 267 | 268 | shouldComponentUpdate(nextProps, nextState) { 269 | return nextProps != this.props || nextState != this.state; 270 | } 271 | 272 | componentWillUpdate(nextProps, nextState) { } 273 | 274 | componentDidUpdate(prevProps, prevState) { } 275 | 276 | componentWillUnmount() { } 277 | } 278 | 279 | 280 | return { 281 | createElement, 282 | render, 283 | Component 284 | } 285 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.16.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component 13 | // 13. passing props to stateful component 14 | // 14. implement setState() method -> parameter as object 15 | // 15. implement the stub for lifecycle method (TODO) 16 | // 16. diffing stateful components (ISSUE: new props coming as #text->object should be toString() in app) 17 | 18 | 19 | const TinyReact = (function () { 20 | function createElement(type, attributes = {}, ...children) { 21 | let childElements = [].concat(...children).reduce( 22 | (acc, child) => { 23 | if (child != null && child !== true && child !== false) { 24 | if (child instanceof Object) { 25 | acc.push(child); 26 | } else { 27 | acc.push(createElement("text", { 28 | textContent: child 29 | })); 30 | } 31 | } 32 | return acc; 33 | } 34 | , []); 35 | return { 36 | type, 37 | children: childElements, 38 | props: Object.assign({ children: childElements }, attributes) 39 | } 40 | } 41 | 42 | const render = function (vdom, container, oldDom = container.firstChild) { 43 | diff(vdom, container, oldDom); 44 | } 45 | 46 | const diff = function (vdom, container, oldDom) { 47 | let oldvdom = oldDom && oldDom._virtualElement; 48 | let oldComponent = oldvdom && oldvdom.component; 49 | 50 | if (!oldDom) { 51 | mountElement(vdom, container, oldDom); 52 | } 53 | else if (typeof vdom.type === "function") { 54 | diffComponent(vdom, oldComponent, container, oldDom); 55 | } 56 | else if (oldvdom && oldvdom.type === vdom.type) { 57 | if (oldvdom.type === "text") { 58 | updateTextNode(oldDom, vdom, oldvdom); 59 | } else { 60 | updateDomElement(oldDom, vdom, oldvdom); 61 | } 62 | 63 | // Set a reference to updated vdom 64 | oldDom._virtualElement = vdom; 65 | 66 | // Recursively diff children.. 67 | // Doing an index by index diffing (because we don't have keys yet) 68 | vdom.children.forEach((child, i) => { 69 | diff(child, oldDom, oldDom.childNodes[i]); 70 | }); 71 | 72 | // Remove old dom nodes 73 | let oldNodes = oldDom.childNodes; 74 | if (oldNodes.length > vdom.children.length) { 75 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 76 | let nodeToBeRemoved = oldNodes[i]; 77 | unmountNode(nodeToBeRemoved, oldDom); 78 | } 79 | } 80 | 81 | } 82 | } 83 | 84 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 85 | 86 | if (isSameComponentType(oldComponent, newVirtualElement)) { 87 | updateComponent(newVirtualElement, oldComponent, container, domElement); 88 | } else { 89 | mountElement(newVirtualElement, container, domElement); 90 | } 91 | } 92 | 93 | function updateComponent(newVirtualElement, oldComponent, container, domElement) { 94 | 95 | // Lifecycle method 96 | oldComponent.componentWillReceiveProps(newVirtualElement.props); 97 | 98 | // Lifecycle method 99 | if (oldComponent.shouldComponentUpdate(newVirtualElement.props)) { 100 | const prevProps = oldComponent.props; 101 | 102 | // Invoke LifeCycle 103 | oldComponent.componentWillUpdate( 104 | newVirtualElement.props, 105 | oldComponent.state 106 | ); 107 | 108 | // Update component 109 | oldComponent.updateProps(newVirtualElement.props); 110 | 111 | // Call Render 112 | // Generate new vdom 113 | const nextElement = oldComponent.render(); 114 | nextElement.component = oldComponent; 115 | 116 | // Recursively diff again 117 | diff(nextElement, container, domElement, oldComponent); 118 | 119 | // Invoke LifeCycle 120 | oldComponent.componentDidUpdate(prevProps); 121 | 122 | } 123 | 124 | } 125 | 126 | function isSameComponentType(oldComponent, newVirtualElement) { 127 | return oldComponent && newVirtualElement.type === oldComponent.constructor; 128 | } 129 | 130 | const mountElement = function (vdom, container, oldDom) { 131 | if (isFunction(vdom)) { 132 | return mountComponent(vdom, container, oldDom); 133 | } else { 134 | return mountSimpleNode(vdom, container, oldDom); 135 | } 136 | } 137 | 138 | function isFunction(obj) { 139 | return obj && 'function' === typeof obj.type; 140 | } 141 | 142 | function isFunctionalComponent(vnode) { 143 | let nodeType = vnode && vnode.type; 144 | return nodeType && isFunction(vnode) 145 | && !(nodeType.prototype && nodeType.prototype.render); 146 | } 147 | 148 | function buildFunctionalComponent(vnode, context) { 149 | return vnode.type(vnode.props || {}); 150 | } 151 | 152 | 153 | function buildStatefulComponent(virtualElement) { 154 | const component = new virtualElement.type(virtualElement.props); 155 | const nextElement = component.render(); 156 | nextElement.component = component; 157 | return nextElement; 158 | } 159 | 160 | function mountComponent(vdom, container, oldDomElement) { 161 | let nextvDom = null, component = null, newDomElement = null; 162 | if (isFunctionalComponent(vdom)) { 163 | nextvDom = buildFunctionalComponent(vdom); 164 | } else { 165 | nextvDom = buildStatefulComponent(vdom); 166 | } 167 | 168 | // Recursively render child components 169 | if (isFunction(nextvDom)) { 170 | return mountComponent(nextvDom, container, oldDomElement); 171 | } else { 172 | newDomElement = mountElement(nextvDom, container, oldDomElement); 173 | } 174 | return newDomElement; 175 | 176 | } 177 | 178 | function unmountNode(domElement, parentComponent) { 179 | domElement.remove(); 180 | } 181 | 182 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 183 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 184 | domElement.textContent = newVirtualElement.props.textContent; 185 | } 186 | // Set a reference to the newvddom in oldDom 187 | domElement._virtualElement = newVirtualElement; 188 | } 189 | 190 | 191 | 192 | 193 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 194 | let newDomElement = null; 195 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 196 | 197 | if (vdom.type === "text") { 198 | newDomElement = document.createTextNode(vdom.props.textContent); 199 | } else { 200 | newDomElement = document.createElement(vdom.type); 201 | updateDomElement(newDomElement, vdom); 202 | } 203 | 204 | // Setting reference to vdom to dom 205 | newDomElement._virtualElement = vdom; 206 | 207 | // TODO: Remove old nodes 208 | if (oldDomElement) { 209 | unmountNode(oldDomElement, parentComponent); 210 | } 211 | 212 | if (nextSibling) { 213 | container.insertBefore(newDomElement, nextSibling); 214 | } else { 215 | container.appendChild(newDomElement); 216 | } 217 | 218 | let component = vdom.component; 219 | if (component) { 220 | component.setDomElement(newDomElement); 221 | } 222 | 223 | vdom.children.forEach(child => { 224 | mountElement(child, newDomElement); 225 | }); 226 | 227 | } 228 | 229 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 230 | const newProps = newVirtualElement.props || {}; 231 | const oldProps = oldVirtualElement.props || {}; 232 | Object.keys(newProps).forEach(propName => { 233 | const newProp = newProps[propName]; 234 | const oldProp = oldProps[propName]; 235 | if (newProp !== oldProp) { 236 | if (propName.slice(0, 2) === "on") { 237 | // prop is an event handler 238 | const eventName = propName.toLowerCase().slice(2); 239 | domElement.addEventListener(eventName, newProp, false); 240 | if (oldProp) { 241 | domElement.removeEventListener(eventName, oldProp, false); 242 | } 243 | } else if (propName === "value" || propName === "checked") { 244 | // this are special attributes that cannot be set 245 | // using setAttribute 246 | domElement[propName] = newProp; 247 | } else if (propName !== "children") { 248 | // ignore the 'children' prop 249 | if (propName === "className") { 250 | domElement.setAttribute("class", newProps[propName]); 251 | } else { 252 | domElement.setAttribute(propName, newProps[propName]); 253 | } 254 | } 255 | } 256 | }); 257 | // remove oldProps 258 | Object.keys(oldProps).forEach(propName => { 259 | const newProp = newProps[propName]; 260 | const oldProp = oldProps[propName]; 261 | if (!newProp) { 262 | if (propName.slice(0, 2) === "on") { 263 | // prop is an event handler 264 | domElement.removeEventListener(propName, oldProp, false); 265 | } else if (propName !== "children") { 266 | // ignore the 'children' prop 267 | domElement.removeAttribute(propName); 268 | } 269 | } 270 | }); 271 | } 272 | 273 | class Component { 274 | constructor(props) { 275 | this.props = props; 276 | this.state = {}; 277 | this.prevState = {}; 278 | } 279 | 280 | setState(nextState) { 281 | if (!this.prevState) this.prevState = this.state; 282 | 283 | this.state = Object.assign({}, this.state, nextState); 284 | 285 | let dom = this.getDomElement(); 286 | let container = dom.parentNode; 287 | 288 | let newvdom = this.render(); 289 | 290 | // Recursively diff 291 | diff(newvdom, container, dom); 292 | 293 | } 294 | 295 | // Helper methods 296 | setDomElement(dom) { 297 | this._dom = dom; 298 | } 299 | 300 | getDomElement() { 301 | return this._dom; 302 | } 303 | 304 | updateProps(props) { 305 | this.props = props; 306 | } 307 | 308 | // Lifecycle methods 309 | componentWillMount() { } 310 | componentDidMount() { } 311 | componentWillReceiveProps(nextProps) { } 312 | 313 | shouldComponentUpdate(nextProps, nextState) { 314 | return nextProps != this.props || nextState != this.state; 315 | } 316 | 317 | componentWillUpdate(nextProps, nextState) { } 318 | 319 | componentDidUpdate(prevProps, prevState) { } 320 | 321 | componentWillUnmount() { } 322 | } 323 | 324 | 325 | return { 326 | createElement, 327 | render, 328 | Component 329 | } 330 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.17.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component 13 | // 13. passing props to stateful component 14 | // 14. implement setState() method -> parameter as object 15 | // 15. implement the stub for lifecycle method (TODO) 16 | // 16. diffing stateful components (ISSUE: new props coming as #text->object should be toString() in app) 17 | // 17. Adding support for ref (todo) 18 | 19 | const TinyReact = (function () { 20 | function createElement(type, attributes = {}, ...children) { 21 | let childElements = [].concat(...children).reduce( 22 | (acc, child) => { 23 | if (child != null && child !== true && child !== false) { 24 | if (child instanceof Object) { 25 | acc.push(child); 26 | } else { 27 | acc.push(createElement("text", { 28 | textContent: child 29 | })); 30 | } 31 | } 32 | return acc; 33 | } 34 | , []); 35 | return { 36 | type, 37 | children: childElements, 38 | props: Object.assign({ children: childElements }, attributes) 39 | } 40 | } 41 | 42 | const render = function (vdom, container, oldDom = container.firstChild) { 43 | diff(vdom, container, oldDom); 44 | } 45 | 46 | const diff = function (vdom, container, oldDom) { 47 | let oldvdom = oldDom && oldDom._virtualElement; 48 | let oldComponent = oldvdom && oldvdom.component; 49 | 50 | if (!oldDom) { 51 | mountElement(vdom, container, oldDom); 52 | } 53 | else if (typeof vdom.type === "function") { 54 | diffComponent(vdom, oldComponent, container, oldDom); 55 | } 56 | else if (oldvdom && oldvdom.type === vdom.type) { 57 | if (oldvdom.type === "text") { 58 | updateTextNode(oldDom, vdom, oldvdom); 59 | } else { 60 | updateDomElement(oldDom, vdom, oldvdom); 61 | } 62 | 63 | // Set a reference to updated vdom 64 | oldDom._virtualElement = vdom; 65 | 66 | // Recursively diff children.. 67 | // Doing an index by index diffing (because we don't have keys yet) 68 | vdom.children.forEach((child, i) => { 69 | diff(child, oldDom, oldDom.childNodes[i]); 70 | }); 71 | 72 | // Remove old dom nodes 73 | let oldNodes = oldDom.childNodes; 74 | if (oldNodes.length > vdom.children.length) { 75 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 76 | let nodeToBeRemoved = oldNodes[i]; 77 | unmountNode(nodeToBeRemoved, oldDom); 78 | } 79 | } 80 | 81 | } 82 | } 83 | 84 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 85 | 86 | if (isSameComponentType(oldComponent, newVirtualElement)) { 87 | updateComponent(newVirtualElement, oldComponent, container, domElement); 88 | } else { 89 | mountElement(newVirtualElement, container, domElement); 90 | } 91 | } 92 | 93 | function updateComponent(newVirtualElement, oldComponent, container, domElement) { 94 | 95 | // Lifecycle method 96 | oldComponent.componentWillReceiveProps(newVirtualElement.props); 97 | 98 | // Lifecycle method 99 | if (oldComponent.shouldComponentUpdate(newVirtualElement.props)) { 100 | const prevProps = oldComponent.props; 101 | 102 | // Invoke LifeCycle 103 | oldComponent.componentWillUpdate( 104 | newVirtualElement.props, 105 | oldComponent.state 106 | ); 107 | 108 | // Update component 109 | oldComponent.updateProps(newVirtualElement.props); 110 | 111 | // Call Render 112 | // Generate new vdom 113 | const nextElement = oldComponent.render(); 114 | nextElement.component = oldComponent; 115 | 116 | // Recursively diff again 117 | diff(nextElement, container, domElement, oldComponent); 118 | 119 | // Invoke LifeCycle 120 | oldComponent.componentDidUpdate(prevProps); 121 | 122 | } 123 | 124 | } 125 | 126 | function isSameComponentType(oldComponent, newVirtualElement) { 127 | return oldComponent && newVirtualElement.type === oldComponent.constructor; 128 | } 129 | 130 | const mountElement = function (vdom, container, oldDom) { 131 | if (isFunction(vdom)) { 132 | return mountComponent(vdom, container, oldDom); 133 | } else { 134 | return mountSimpleNode(vdom, container, oldDom); 135 | } 136 | } 137 | 138 | function isFunction(obj) { 139 | return obj && 'function' === typeof obj.type; 140 | } 141 | 142 | function isFunctionalComponent(vnode) { 143 | let nodeType = vnode && vnode.type; 144 | return nodeType && isFunction(vnode) 145 | && !(nodeType.prototype && nodeType.prototype.render); 146 | } 147 | 148 | function buildFunctionalComponent(vnode, context) { 149 | return vnode.type(vnode.props || {}); 150 | } 151 | 152 | 153 | function buildStatefulComponent(virtualElement) { 154 | const component = new virtualElement.type(virtualElement.props); 155 | const nextElement = component.render(); 156 | nextElement.component = component; 157 | return nextElement; 158 | } 159 | 160 | function mountComponent(vdom, container, oldDomElement) { 161 | let nextvDom = null, component = null, newDomElement = null; 162 | if (isFunctionalComponent(vdom)) { 163 | nextvDom = buildFunctionalComponent(vdom); 164 | } else { 165 | nextvDom = buildStatefulComponent(vdom); 166 | component = nextvDom.component; 167 | } 168 | 169 | // Recursively render child components 170 | if (isFunction(nextvDom)) { 171 | return mountComponent(nextvDom, container, oldDomElement); 172 | } else { 173 | newDomElement = mountElement(nextvDom, container, oldDomElement); 174 | } 175 | 176 | if (component) { 177 | component.componentDidMount(); // Life cycle method 178 | if (component.props.ref) { 179 | component.props.ref(component); 180 | } 181 | } 182 | 183 | return newDomElement; 184 | } 185 | 186 | function unmountNode(domElement, parentComponent) { 187 | const virtualElement = domElement._virtualElement; 188 | if (!virtualElement) { 189 | domElement.remove(); 190 | return; 191 | } 192 | 193 | if (virtualElement.props && virtualElement.props.ref) { 194 | virtualElement.props.ref(null); 195 | } 196 | 197 | } 198 | 199 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 200 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 201 | domElement.textContent = newVirtualElement.props.textContent; 202 | } 203 | // Set a reference to the newvddom in oldDom 204 | domElement._virtualElement = newVirtualElement; 205 | } 206 | 207 | 208 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 209 | let newDomElement = null; 210 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 211 | 212 | if (vdom.type === "text") { 213 | newDomElement = document.createTextNode(vdom.props.textContent); 214 | } else { 215 | newDomElement = document.createElement(vdom.type); 216 | updateDomElement(newDomElement, vdom); 217 | } 218 | 219 | // Setting reference to vdom to dom 220 | newDomElement._virtualElement = vdom; 221 | 222 | // TODO: Remove old nodes 223 | if (oldDomElement) { 224 | unmountNode(oldDomElement, parentComponent); 225 | } 226 | 227 | if (nextSibling) { 228 | container.insertBefore(newDomElement, nextSibling); 229 | } else { 230 | container.appendChild(newDomElement); 231 | } 232 | 233 | let component = vdom.component; 234 | if (component) { 235 | component.setDomElement(newDomElement); 236 | } 237 | 238 | vdom.children.forEach(child => { 239 | mountElement(child, newDomElement); 240 | }); 241 | 242 | if (vdom.props && vdom.props.ref) { 243 | vdom.props.ref(newDomElement); 244 | } 245 | 246 | } 247 | 248 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 249 | const newProps = newVirtualElement.props || {}; 250 | const oldProps = oldVirtualElement.props || {}; 251 | Object.keys(newProps).forEach(propName => { 252 | const newProp = newProps[propName]; 253 | const oldProp = oldProps[propName]; 254 | if (newProp !== oldProp) { 255 | if (propName.slice(0, 2) === "on") { 256 | // prop is an event handler 257 | const eventName = propName.toLowerCase().slice(2); 258 | domElement.addEventListener(eventName, newProp, false); 259 | if (oldProp) { 260 | domElement.removeEventListener(eventName, oldProp, false); 261 | } 262 | } else if (propName === "value" || propName === "checked") { 263 | // this are special attributes that cannot be set 264 | // using setAttribute 265 | domElement[propName] = newProp; 266 | } else if (propName !== "children") { 267 | // ignore the 'children' prop 268 | if (propName === "className") { 269 | domElement.setAttribute("class", newProps[propName]); 270 | } else { 271 | domElement.setAttribute(propName, newProps[propName]); 272 | } 273 | } 274 | } 275 | }); 276 | // remove oldProps 277 | Object.keys(oldProps).forEach(propName => { 278 | const newProp = newProps[propName]; 279 | const oldProp = oldProps[propName]; 280 | if (!newProp) { 281 | if (propName.slice(0, 2) === "on") { 282 | // prop is an event handler 283 | domElement.removeEventListener(propName, oldProp, false); 284 | } else if (propName !== "children") { 285 | // ignore the 'children' prop 286 | domElement.removeAttribute(propName); 287 | } 288 | } 289 | }); 290 | } 291 | 292 | class Component { 293 | constructor(props) { 294 | this.props = props; 295 | this.state = {}; 296 | this.prevState = {}; 297 | } 298 | 299 | setState(nextState) { 300 | if (!this.prevState) this.prevState = this.state; 301 | 302 | this.state = Object.assign({}, this.state, nextState); 303 | 304 | let dom = this.getDomElement(); 305 | let container = dom.parentNode; 306 | 307 | let newvdom = this.render(); 308 | 309 | // Recursively diff 310 | diff(newvdom, container, dom); 311 | 312 | } 313 | 314 | // Helper methods 315 | setDomElement(dom) { 316 | this._dom = dom; 317 | } 318 | 319 | getDomElement() { 320 | return this._dom; 321 | } 322 | 323 | updateProps(props) { 324 | this.props = props; 325 | } 326 | 327 | // Lifecycle methods 328 | componentWillMount() { } 329 | componentDidMount() { } 330 | componentWillReceiveProps(nextProps) { } 331 | 332 | shouldComponentUpdate(nextProps, nextState) { 333 | return nextProps != this.props || nextState != this.state; 334 | } 335 | 336 | componentWillUpdate(nextProps, nextState) { } 337 | 338 | componentDidUpdate(prevProps, prevState) { } 339 | 340 | componentWillUnmount() { } 341 | } 342 | 343 | 344 | return { 345 | createElement, 346 | render, 347 | Component 348 | } 349 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.18.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component 13 | // 13. passing props to stateful component 14 | // 14. implement setState() method -> parameter as object 15 | // 15. implement the stub for lifecycle method (TODO) 16 | // 16. diffing stateful components (ISSUE: new props coming as #text->object should be toString() in app) 17 | // 17. Adding support for ref (todo) 18 | // 18. More test cases, old and new types are different. 19 | 20 | 21 | const TinyReact = (function () { 22 | function createElement(type, attributes = {}, ...children) { 23 | let childElements = [].concat(...children).reduce( 24 | (acc, child) => { 25 | if (child != null && child !== true && child !== false) { 26 | if (child instanceof Object) { 27 | acc.push(child); 28 | } else { 29 | acc.push(createElement("text", { 30 | textContent: child 31 | })); 32 | } 33 | } 34 | return acc; 35 | } 36 | , []); 37 | return { 38 | type, 39 | children: childElements, 40 | props: Object.assign({ children: childElements }, attributes) 41 | } 42 | } 43 | 44 | const render = function (vdom, container, oldDom = container.firstChild) { 45 | diff(vdom, container, oldDom); 46 | } 47 | 48 | const diff = function (vdom, container, oldDom) { 49 | let oldvdom = oldDom && oldDom._virtualElement; 50 | let oldComponent = oldvdom && oldvdom.component; 51 | 52 | if (!oldDom) { 53 | mountElement(vdom, container, oldDom); 54 | } 55 | else if ((vdom.type !== oldvdom.type) && (typeof vdom.type !== "function")) { 56 | let newDomElement = createDomElement(vdom); // todo: 57 | oldDom.parentNode.replaceChild(newDomElement, oldDom); 58 | } 59 | else if (typeof vdom.type === "function") { 60 | diffComponent(vdom, oldComponent, container, oldDom); 61 | } 62 | else if (oldvdom && oldvdom.type === vdom.type) { 63 | if (oldvdom.type === "text") { 64 | updateTextNode(oldDom, vdom, oldvdom); 65 | } else { 66 | updateDomElement(oldDom, vdom, oldvdom); 67 | } 68 | 69 | // Set a reference to updated vdom 70 | oldDom._virtualElement = vdom; 71 | 72 | // Recursively diff children.. 73 | // Doing an index by index diffing (because we don't have keys yet) 74 | vdom.children.forEach((child, i) => { 75 | diff(child, oldDom, oldDom.childNodes[i]); 76 | }); 77 | 78 | // Remove old dom nodes 79 | let oldNodes = oldDom.childNodes; 80 | if (oldNodes.length > vdom.children.length) { 81 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 82 | let nodeToBeRemoved = oldNodes[i]; 83 | unmountNode(nodeToBeRemoved, oldDom); 84 | } 85 | } 86 | 87 | } 88 | } 89 | 90 | function createDomElement(vdom) { 91 | let newDomElement = null; 92 | if (vdom.type === "text") { 93 | newDomElement = document.createTextNode(vdom.props.textContent); 94 | } else { 95 | newDomElement = document.createElement(vdom.type); 96 | updateDomElement(newDomElement, vdom); 97 | } 98 | 99 | newDomElement._virtualElement = vdom; 100 | vdom.children.forEach((child) => { 101 | newDomElement.appendChild(createDomElement(child)); 102 | }); 103 | 104 | // Set refs 105 | if (vdom.props && vdom.props.ref) { 106 | vdom.props.ref(newDomElement); 107 | } 108 | 109 | return newDomElement; 110 | } 111 | 112 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 113 | 114 | if (isSameComponentType(oldComponent, newVirtualElement)) { 115 | updateComponent(newVirtualElement, oldComponent, container, domElement); 116 | } else { 117 | mountElement(newVirtualElement, container, domElement); 118 | } 119 | } 120 | 121 | function updateComponent(newVirtualElement, oldComponent, container, domElement) { 122 | 123 | // Lifecycle method 124 | oldComponent.componentWillReceiveProps(newVirtualElement.props); 125 | 126 | // Lifecycle method 127 | if (oldComponent.shouldComponentUpdate(newVirtualElement.props)) { 128 | const prevProps = oldComponent.props; 129 | 130 | // Invoke LifeCycle 131 | oldComponent.componentWillUpdate( 132 | newVirtualElement.props, 133 | oldComponent.state 134 | ); 135 | 136 | // Update component 137 | oldComponent.updateProps(newVirtualElement.props); 138 | 139 | // Call Render 140 | // Generate new vdom 141 | const nextElement = oldComponent.render(); 142 | nextElement.component = oldComponent; 143 | 144 | // Recursively diff again 145 | diff(nextElement, container, domElement, oldComponent); 146 | 147 | // Invoke LifeCycle 148 | oldComponent.componentDidUpdate(prevProps); 149 | 150 | } 151 | 152 | } 153 | 154 | function isSameComponentType(oldComponent, newVirtualElement) { 155 | return oldComponent && newVirtualElement.type === oldComponent.constructor; 156 | } 157 | 158 | const mountElement = function (vdom, container, oldDom) { 159 | if (isFunction(vdom)) { 160 | return mountComponent(vdom, container, oldDom); 161 | } else { 162 | return mountSimpleNode(vdom, container, oldDom); 163 | } 164 | } 165 | 166 | function isFunction(obj) { 167 | return obj && 'function' === typeof obj.type; 168 | } 169 | 170 | function isFunctionalComponent(vnode) { 171 | let nodeType = vnode && vnode.type; 172 | return nodeType && isFunction(vnode) 173 | && !(nodeType.prototype && nodeType.prototype.render); 174 | } 175 | 176 | function buildFunctionalComponent(vnode, context) { 177 | return vnode.type(vnode.props || {}); 178 | } 179 | 180 | 181 | function buildStatefulComponent(virtualElement) { 182 | const component = new virtualElement.type(virtualElement.props); 183 | const nextElement = component.render(); 184 | nextElement.component = component; 185 | return nextElement; 186 | } 187 | 188 | function mountComponent(vdom, container, oldDomElement) { 189 | let nextvDom = null, component = null, newDomElement = null; 190 | if (isFunctionalComponent(vdom)) { 191 | nextvDom = buildFunctionalComponent(vdom); 192 | } else { 193 | nextvDom = buildStatefulComponent(vdom); 194 | component = nextvDom.component; 195 | } 196 | 197 | // Recursively render child components 198 | if (isFunction(nextvDom)) { 199 | return mountComponent(nextvDom, container, oldDomElement); 200 | } else { 201 | newDomElement = mountElement(nextvDom, container, oldDomElement); 202 | } 203 | 204 | if (component) { 205 | component.componentDidMount(); // Life cycle method 206 | if (component.props.ref) { 207 | component.props.ref(component); 208 | } 209 | } 210 | 211 | return newDomElement; 212 | } 213 | 214 | function unmountNode(domElement, parentComponent) { 215 | const virtualElement = domElement._virtualElement; 216 | if (!virtualElement) { 217 | domElement.remove(); 218 | return; 219 | } 220 | 221 | // If component exist 222 | let oldComponent = domElement._virtualElement.component; 223 | if (oldComponent) { 224 | oldComponent.componentWillUnmount(); 225 | } 226 | 227 | // Recursive calls agains 228 | while (domElement.childNodes.length > 0) { 229 | unmountNode(domElement.firstChild); 230 | } 231 | 232 | if (virtualElement.props && virtualElement.props.ref) { 233 | virtualElement.props.ref(null); 234 | } 235 | 236 | // Clear out event handlers 237 | Object.keys(virtualElement.props).forEach(propName => { 238 | if (propName.slice(0, 2) === "on") { 239 | const event = propName.toLowerCase().slice(2); 240 | const handler = virtualElement.props[propName]; 241 | domElement.removeEventListener(event, handler); 242 | } 243 | }); 244 | 245 | domElement.remove(); 246 | } 247 | 248 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 249 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 250 | domElement.textContent = newVirtualElement.props.textContent; 251 | } 252 | // Set a reference to the newvddom in oldDom 253 | domElement._virtualElement = newVirtualElement; 254 | } 255 | 256 | 257 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 258 | let newDomElement = null; 259 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 260 | 261 | if (vdom.type === "text") { 262 | newDomElement = document.createTextNode(vdom.props.textContent); 263 | } else { 264 | newDomElement = document.createElement(vdom.type); 265 | updateDomElement(newDomElement, vdom); 266 | } 267 | 268 | // Setting reference to vdom to dom 269 | newDomElement._virtualElement = vdom; 270 | 271 | // TODO: Remove old nodes 272 | if (oldDomElement) { 273 | unmountNode(oldDomElement, parentComponent); 274 | } 275 | 276 | if (nextSibling) { 277 | container.insertBefore(newDomElement, nextSibling); 278 | } else { 279 | container.appendChild(newDomElement); 280 | } 281 | 282 | let component = vdom.component; 283 | if (component) { 284 | component.setDomElement(newDomElement); 285 | } 286 | 287 | vdom.children.forEach(child => { 288 | mountElement(child, newDomElement); 289 | }); 290 | 291 | if (vdom.props && vdom.props.ref) { 292 | vdom.props.ref(newDomElement); 293 | } 294 | 295 | } 296 | 297 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 298 | const newProps = newVirtualElement.props || {}; 299 | const oldProps = oldVirtualElement.props || {}; 300 | Object.keys(newProps).forEach(propName => { 301 | const newProp = newProps[propName]; 302 | const oldProp = oldProps[propName]; 303 | if (newProp !== oldProp) { 304 | if (propName.slice(0, 2) === "on") { 305 | // prop is an event handler 306 | const eventName = propName.toLowerCase().slice(2); 307 | domElement.addEventListener(eventName, newProp, false); 308 | if (oldProp) { 309 | domElement.removeEventListener(eventName, oldProp, false); 310 | } 311 | } else if (propName === "value" || propName === "checked") { 312 | // this are special attributes that cannot be set 313 | // using setAttribute 314 | domElement[propName] = newProp; 315 | } else if (propName !== "children") { 316 | // ignore the 'children' prop 317 | if (propName === "className") { 318 | domElement.setAttribute("class", newProps[propName]); 319 | } else { 320 | domElement.setAttribute(propName, newProps[propName]); 321 | } 322 | } 323 | } 324 | }); 325 | // remove oldProps 326 | Object.keys(oldProps).forEach(propName => { 327 | const newProp = newProps[propName]; 328 | const oldProp = oldProps[propName]; 329 | if (!newProp) { 330 | if (propName.slice(0, 2) === "on") { 331 | // prop is an event handler 332 | domElement.removeEventListener(propName, oldProp, false); 333 | } else if (propName !== "children") { 334 | // ignore the 'children' prop 335 | domElement.removeAttribute(propName); 336 | } 337 | } 338 | }); 339 | } 340 | 341 | class Component { 342 | constructor(props) { 343 | this.props = props; 344 | this.state = {}; 345 | this.prevState = {}; 346 | } 347 | 348 | setState(nextState) { 349 | if (!this.prevState) this.prevState = this.state; 350 | 351 | this.state = Object.assign({}, this.state, nextState); 352 | 353 | let dom = this.getDomElement(); 354 | let container = dom.parentNode; 355 | 356 | let newvdom = this.render(); 357 | 358 | // Recursively diff 359 | diff(newvdom, container, dom); 360 | 361 | } 362 | 363 | // Helper methods 364 | setDomElement(dom) { 365 | this._dom = dom; 366 | } 367 | 368 | getDomElement() { 369 | return this._dom; 370 | } 371 | 372 | updateProps(props) { 373 | this.props = props; 374 | } 375 | 376 | // Lifecycle methods 377 | componentWillMount() { } 378 | componentDidMount() { } 379 | componentWillReceiveProps(nextProps) { } 380 | 381 | shouldComponentUpdate(nextProps, nextState) { 382 | return nextProps != this.props || nextState != this.state; 383 | } 384 | 385 | componentWillUpdate(nextProps, nextState) { } 386 | 387 | componentDidUpdate(prevProps, prevState) { } 388 | 389 | componentWillUnmount() { } 390 | } 391 | 392 | 393 | return { 394 | createElement, 395 | render, 396 | Component 397 | } 398 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.19.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component 13 | // 13. passing props to stateful component 14 | // 14. implement setState() method -> parameter as object 15 | // 15. implement the stub for lifecycle method (TODO) 16 | // 16. diffing stateful components (ISSUE: new props coming as #text->object should be toString() in app) 17 | // 17. Adding support for ref (todo) 18 | // 18. More test cases, old and new types are different. 19 | // 19. Add support for passing style attributes as objects 20 | 21 | 22 | const TinyReact = (function () { 23 | function createElement(type, attributes = {}, ...children) { 24 | let childElements = [].concat(...children).reduce( 25 | (acc, child) => { 26 | if (child != null && child !== true && child !== false) { 27 | if (child instanceof Object) { 28 | acc.push(child); 29 | } else { 30 | acc.push(createElement("text", { 31 | textContent: child 32 | })); 33 | } 34 | } 35 | return acc; 36 | } 37 | , []); 38 | return { 39 | type, 40 | children: childElements, 41 | props: Object.assign({ children: childElements }, attributes) 42 | } 43 | } 44 | 45 | const render = function (vdom, container, oldDom = container.firstChild) { 46 | diff(vdom, container, oldDom); 47 | } 48 | 49 | const diff = function (vdom, container, oldDom) { 50 | let oldvdom = oldDom && oldDom._virtualElement; 51 | let oldComponent = oldvdom && oldvdom.component; 52 | 53 | if (!oldDom) { 54 | mountElement(vdom, container, oldDom); 55 | } 56 | else if ((vdom.type !== oldvdom.type) && (typeof vdom.type !== "function")) { 57 | let newDomElement = createDomElement(vdom); // todo: 58 | oldDom.parentNode.replaceChild(newDomElement, oldDom); 59 | } 60 | else if (typeof vdom.type === "function") { 61 | diffComponent(vdom, oldComponent, container, oldDom); 62 | } 63 | else if (oldvdom && oldvdom.type === vdom.type) { 64 | if (oldvdom.type === "text") { 65 | updateTextNode(oldDom, vdom, oldvdom); 66 | } else { 67 | updateDomElement(oldDom, vdom, oldvdom); 68 | } 69 | 70 | // Set a reference to updated vdom 71 | oldDom._virtualElement = vdom; 72 | 73 | // Lets create a collection of keyed elements 74 | let keyedElements = {}; 75 | for (let i = 0; i < oldDom.childNodes.length; i += 1) { 76 | const domElement = oldDom.childNodes[i]; 77 | const key = domElement._virtualElement.props.key; 78 | 79 | if (key) { 80 | keyedElements[key] = { 81 | domElement, 82 | index: i 83 | }; 84 | } 85 | 86 | } 87 | 88 | 89 | // Recursively diff children.. 90 | // Doing an index by index diffing (because we don't have keys yet) 91 | if (Object.keys(keyedElements).length === 0) { 92 | vdom.children.forEach((child, i) => { 93 | diff(child, oldDom, oldDom.childNodes[i]); 94 | }); 95 | } else { 96 | // Reconciliation based on keys 97 | vdom.children.forEach((virtualElement, i) => { 98 | const key = virtualElement.props.key; 99 | if (key) { 100 | const keyedDomElement = keyedElements[key]; 101 | if (keyedDomElement) { 102 | // Position the new element correctly based on key/index 103 | if (oldDom.childNodes[i] && !oldDom.childNodes[i].isSameNode(keyedDomElement.domElement)) { 104 | oldDom.insertBefore(keyedDomElement.domElement, 105 | oldDom.childNodes[i]); 106 | } 107 | diff(virtualElement, oldDom, keyedDomElement.domElement); 108 | } 109 | else { 110 | mountElement(virtualElement, oldDom); 111 | } 112 | } 113 | }); 114 | } 115 | 116 | // Remove old dom nodes 117 | let oldNodes = oldDom.childNodes; 118 | if (Object.keys(keyedElements).length === 0) { 119 | if (oldNodes.length > vdom.children.length) { 120 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 121 | let nodeToBeRemoved = oldNodes[i]; 122 | unmountNode(nodeToBeRemoved, oldDom); 123 | } 124 | } 125 | } else { 126 | if (oldNodes.length > vdom.children.length) { 127 | for (let i = 0; i < oldDom.childNodes.length; i += 1) { 128 | let oldChild = oldDom.childNodes[i]; 129 | let oldKey = oldChild.getAttribute("key"); 130 | 131 | let found = false; 132 | for (let n = 0; n < vdom.children.length; n += 1) { 133 | if (vdom.children[n].props.key == oldKey) { 134 | found = true; 135 | break; 136 | } 137 | } 138 | 139 | if (!found) { 140 | unmountNode(oldChild, oldDom); 141 | } 142 | 143 | } 144 | } 145 | } 146 | 147 | } 148 | } 149 | 150 | function createDomElement(vdom) { 151 | let newDomElement = null; 152 | if (vdom.type === "text") { 153 | newDomElement = document.createTextNode(vdom.props.textContent); 154 | } else { 155 | newDomElement = document.createElement(vdom.type); 156 | updateDomElement(newDomElement, vdom); 157 | } 158 | 159 | newDomElement._virtualElement = vdom; 160 | vdom.children.forEach((child) => { 161 | newDomElement.appendChild(createDomElement(child)); 162 | }); 163 | 164 | // Set refs 165 | if (vdom.props && vdom.props.ref) { 166 | vdom.props.ref(newDomElement); 167 | } 168 | 169 | return newDomElement; 170 | } 171 | 172 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 173 | 174 | if (isSameComponentType(oldComponent, newVirtualElement)) { 175 | updateComponent(newVirtualElement, oldComponent, container, domElement); 176 | } else { 177 | mountElement(newVirtualElement, container, domElement); 178 | } 179 | } 180 | 181 | function updateComponent(newVirtualElement, oldComponent, container, domElement) { 182 | 183 | // Lifecycle method 184 | oldComponent.componentWillReceiveProps(newVirtualElement.props); 185 | 186 | // Lifecycle method 187 | if (oldComponent.shouldComponentUpdate(newVirtualElement.props)) { 188 | const prevProps = oldComponent.props; 189 | 190 | // Invoke LifeCycle 191 | oldComponent.componentWillUpdate( 192 | newVirtualElement.props, 193 | oldComponent.state 194 | ); 195 | 196 | // Update component 197 | oldComponent.updateProps(newVirtualElement.props); 198 | 199 | // Call Render 200 | // Generate new vdom 201 | const nextElement = oldComponent.render(); 202 | nextElement.component = oldComponent; 203 | 204 | // Recursively diff again 205 | diff(nextElement, container, domElement, oldComponent); 206 | 207 | // Invoke LifeCycle 208 | oldComponent.componentDidUpdate(prevProps); 209 | 210 | } 211 | 212 | } 213 | 214 | function isSameComponentType(oldComponent, newVirtualElement) { 215 | return oldComponent && newVirtualElement.type === oldComponent.constructor; 216 | } 217 | 218 | const mountElement = function (vdom, container, oldDom) { 219 | if (isFunction(vdom)) { 220 | return mountComponent(vdom, container, oldDom); 221 | } else { 222 | return mountSimpleNode(vdom, container, oldDom); 223 | } 224 | } 225 | 226 | function isFunction(obj) { 227 | return obj && 'function' === typeof obj.type; 228 | } 229 | 230 | function isFunctionalComponent(vnode) { 231 | let nodeType = vnode && vnode.type; 232 | return nodeType && isFunction(vnode) 233 | && !(nodeType.prototype && nodeType.prototype.render); 234 | } 235 | 236 | function buildFunctionalComponent(vnode, context) { 237 | return vnode.type(vnode.props || {}); 238 | } 239 | 240 | 241 | function buildStatefulComponent(virtualElement) { 242 | const component = new virtualElement.type(virtualElement.props); 243 | const nextElement = component.render(); 244 | nextElement.component = component; 245 | return nextElement; 246 | } 247 | 248 | function mountComponent(vdom, container, oldDomElement) { 249 | let nextvDom = null, component = null, newDomElement = null; 250 | if (isFunctionalComponent(vdom)) { 251 | nextvDom = buildFunctionalComponent(vdom); 252 | } else { 253 | nextvDom = buildStatefulComponent(vdom); 254 | component = nextvDom.component; 255 | } 256 | 257 | // Recursively render child components 258 | if (isFunction(nextvDom)) { 259 | return mountComponent(nextvDom, container, oldDomElement); 260 | } else { 261 | newDomElement = mountElement(nextvDom, container, oldDomElement); 262 | } 263 | 264 | if (component) { 265 | component.componentDidMount(); // Life cycle method 266 | if (component.props.ref) { 267 | component.props.ref(component); 268 | } 269 | } 270 | 271 | return newDomElement; 272 | } 273 | 274 | function unmountNode(domElement, parentComponent) { 275 | const virtualElement = domElement._virtualElement; 276 | if (!virtualElement) { 277 | domElement.remove(); 278 | return; 279 | } 280 | 281 | // If component exist 282 | let oldComponent = domElement._virtualElement.component; 283 | if (oldComponent) { 284 | oldComponent.componentWillUnmount(); 285 | } 286 | 287 | // Recursive calls agains 288 | while (domElement.childNodes.length > 0) { 289 | unmountNode(domElement.firstChild); 290 | } 291 | 292 | if (virtualElement.props && virtualElement.props.ref) { 293 | virtualElement.props.ref(null); 294 | } 295 | 296 | // Clear out event handlers 297 | Object.keys(virtualElement.props).forEach(propName => { 298 | if (propName.slice(0, 2) === "on") { 299 | const event = propName.toLowerCase().slice(2); 300 | const handler = virtualElement.props[propName]; 301 | domElement.removeEventListener(event, handler); 302 | } 303 | }); 304 | 305 | domElement.remove(); 306 | } 307 | 308 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 309 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 310 | domElement.textContent = newVirtualElement.props.textContent; 311 | } 312 | // Set a reference to the newvddom in oldDom 313 | domElement._virtualElement = newVirtualElement; 314 | } 315 | 316 | 317 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 318 | let newDomElement = null; 319 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 320 | 321 | if (vdom.type === "text") { 322 | newDomElement = document.createTextNode(vdom.props.textContent); 323 | } else { 324 | newDomElement = document.createElement(vdom.type); 325 | updateDomElement(newDomElement, vdom); 326 | } 327 | 328 | // Setting reference to vdom to dom 329 | newDomElement._virtualElement = vdom; 330 | 331 | // TODO: Remove old nodes 332 | if (oldDomElement) { 333 | unmountNode(oldDomElement, parentComponent); 334 | } 335 | 336 | if (nextSibling) { 337 | container.insertBefore(newDomElement, nextSibling); 338 | } else { 339 | container.appendChild(newDomElement); 340 | } 341 | 342 | let component = vdom.component; 343 | if (component) { 344 | component.setDomElement(newDomElement); 345 | } 346 | 347 | vdom.children.forEach(child => { 348 | mountElement(child, newDomElement); 349 | }); 350 | 351 | if (vdom.props && vdom.props.ref) { 352 | vdom.props.ref(newDomElement); 353 | } 354 | 355 | } 356 | 357 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 358 | const newProps = newVirtualElement.props || {}; 359 | const oldProps = oldVirtualElement.props || {}; 360 | Object.keys(newProps).forEach(propName => { 361 | const newProp = newProps[propName]; 362 | const oldProp = oldProps[propName]; 363 | if (newProp !== oldProp) { 364 | if (propName.slice(0, 2) === "on") { 365 | // prop is an event handler 366 | const eventName = propName.toLowerCase().slice(2); 367 | domElement.addEventListener(eventName, newProp, false); 368 | if (oldProp) { 369 | domElement.removeEventListener(eventName, oldProp, false); 370 | } 371 | } else if (propName === "value" || propName === "checked") { 372 | // this are special attributes that cannot be set 373 | // using setAttribute 374 | domElement[propName] = newProp; 375 | } else if (propName !== "children") { 376 | 377 | // ignore the 'children' prop 378 | if (propName === "className") { 379 | domElement.setAttribute("class", newProps[propName]); 380 | } else if (propName === "style" && !newProps[propName].substring) { 381 | let styleText = styleObjToCss(newProps[propName]); 382 | domElement.style = styleText; 383 | } 384 | else { 385 | domElement.setAttribute(propName, newProps[propName]); 386 | } 387 | } 388 | } 389 | }); 390 | // remove oldProps 391 | Object.keys(oldProps).forEach(propName => { 392 | const newProp = newProps[propName]; 393 | const oldProp = oldProps[propName]; 394 | if (!newProp) { 395 | if (propName.slice(0, 2) === "on") { 396 | // prop is an event handler 397 | domElement.removeEventListener(propName, oldProp, false); 398 | } else if (propName !== "children") { 399 | // ignore the 'children' prop 400 | domElement.removeAttribute(propName); 401 | } 402 | } 403 | }); 404 | } 405 | 406 | function styleObjToCss(styleObj) { 407 | // I am not checking for non-dimensional props here 408 | // Assuming the correct dimensional values are passed for e.g. 10px etc 409 | 410 | let styleCss = "", sep = ":", term = ";"; 411 | 412 | for (let prop in styleObj) { 413 | if (styleObj.hasOwnProperty(prop)) { 414 | let val = styleObj[prop]; 415 | styleCss += `${jsToCss(prop)} : ${val} ${term}`; 416 | } 417 | } 418 | 419 | return styleCss; 420 | 421 | } 422 | 423 | function jsToCss(s) { 424 | // OLd preact code base. 425 | let transformedText = s.replace(/([A-Z])/, '-$1').toLowerCase(); 426 | // borderBottom transform to border-bottom 427 | return transformedText; 428 | } 429 | 430 | class Component { 431 | constructor(props) { 432 | this.props = props; 433 | this.state = {}; 434 | this.prevState = {}; 435 | } 436 | 437 | setState(nextState) { 438 | if (!this.prevState) this.prevState = this.state; 439 | 440 | this.state = Object.assign({}, this.state, nextState); 441 | 442 | let dom = this.getDomElement(); 443 | let container = dom.parentNode; 444 | 445 | let newvdom = this.render(); 446 | 447 | // Recursively diff 448 | diff(newvdom, container, dom); 449 | 450 | } 451 | 452 | // Helper methods 453 | setDomElement(dom) { 454 | this._dom = dom; 455 | } 456 | 457 | getDomElement() { 458 | return this._dom; 459 | } 460 | 461 | updateProps(props) { 462 | this.props = props; 463 | } 464 | 465 | // Lifecycle methods 466 | componentWillMount() { } 467 | componentDidMount() { } 468 | componentWillReceiveProps(nextProps) { } 469 | 470 | shouldComponentUpdate(nextProps, nextState) { 471 | return nextProps != this.props || nextState != this.state; 472 | } 473 | 474 | componentWillUpdate(nextProps, nextState) { } 475 | 476 | componentDidUpdate(prevProps, prevState) { } 477 | 478 | componentWillUnmount() { } 479 | } 480 | 481 | 482 | return { 483 | createElement, 484 | render, 485 | Component 486 | } 487 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.2.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement simple implementation 3 | 4 | const TinyReact = (function () { 5 | function createElement(type, attributes = {}, ...children) { 6 | const childElements = [].concat(...children).map( 7 | child => 8 | child instanceof Object 9 | ? child 10 | : createElement("text", { 11 | textContent: child 12 | }) 13 | ); 14 | return { 15 | type, 16 | children: childElements, 17 | props: Object.assign({ children: childElements }, attributes) 18 | } 19 | } 20 | 21 | return { 22 | createElement 23 | } 24 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.3.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | 5 | const TinyReact = (function () { 6 | function createElement(type, attributes = {}, ...children) { 7 | const childElements = [].concat(...children).map( 8 | child => { 9 | if (child != null && child !== true && child !== false) { 10 | return child instanceof Object 11 | ? child 12 | : createElement("text", { 13 | textContent: child 14 | }) 15 | } 16 | } 17 | ); 18 | return { 19 | type, 20 | children: childElements, 21 | props: Object.assign({ children: childElements }, attributes) 22 | } 23 | } 24 | 25 | return { 26 | createElement 27 | } 28 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.4.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | 6 | const TinyReact = (function () { 7 | function createElement(type, attributes = {}, ...children) { 8 | let childElements = [].concat(...children).reduce( 9 | (acc, child) => { 10 | if (child != null && child !== true && child !== false) { 11 | if (child instanceof Object) { 12 | acc.push(child); 13 | } else { 14 | acc.push(createElement("text", { 15 | textContent: child 16 | })); 17 | } 18 | } 19 | return acc; 20 | } 21 | , []); 22 | return { 23 | type, 24 | children: childElements, 25 | props: Object.assign({ children: childElements }, attributes) 26 | } 27 | } 28 | 29 | 30 | return { 31 | createElement 32 | } 33 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.5.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | 7 | const TinyReact = (function () { 8 | function createElement(type, attributes = {}, ...children) { 9 | let childElements = [].concat(...children).reduce( 10 | (acc, child) => { 11 | if (child != null && child !== true && child !== false) { 12 | if (child instanceof Object) { 13 | acc.push(child); 14 | } else { 15 | acc.push(createElement("text", { 16 | textContent: child 17 | })); 18 | } 19 | } 20 | return acc; 21 | } 22 | , []); 23 | return { 24 | type, 25 | children: childElements, 26 | props: Object.assign({ children: childElements }, attributes) 27 | } 28 | } 29 | 30 | const render = function (vdom, container, oldDom = container.firstChild) { 31 | if (!oldDom) { 32 | mountElement(vdom, container, oldDom); 33 | } 34 | } 35 | 36 | const mountElement = function (vdom, container, oldDom) { 37 | // Native DOM elements as well as functions. 38 | return mountSimpleNode(vdom, container, oldDom); 39 | } 40 | 41 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 42 | let newDomElement = null; 43 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 44 | 45 | if (vdom.type === "text") { 46 | newDomElement = document.createTextNode(vdom.props.textContent); 47 | } else { 48 | newDomElement = document.createElement(vdom.type); 49 | } 50 | 51 | // Setting reference to vdom to dom 52 | newDomElement._virtualElement = vdom; 53 | if (nextSibling) { 54 | container.insertBefore(newDomElement, nextSibling); 55 | } else { 56 | container.appendChild(newDomElement); 57 | } 58 | 59 | //TODO: Render children 60 | vdom.children.forEach(child => { 61 | mountElement(child, newDomElement); 62 | }); 63 | 64 | } 65 | 66 | 67 | return { 68 | createElement, 69 | render 70 | } 71 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.6.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | 8 | const TinyReact = (function () { 9 | function createElement(type, attributes = {}, ...children) { 10 | let childElements = [].concat(...children).reduce( 11 | (acc, child) => { 12 | if (child != null && child !== true && child !== false) { 13 | if (child instanceof Object) { 14 | acc.push(child); 15 | } else { 16 | acc.push(createElement("text", { 17 | textContent: child 18 | })); 19 | } 20 | } 21 | return acc; 22 | } 23 | , []); 24 | return { 25 | type, 26 | children: childElements, 27 | props: Object.assign({ children: childElements }, attributes) 28 | } 29 | } 30 | 31 | const render = function (vdom, container, oldDom = container.firstChild) { 32 | if (!oldDom) { 33 | mountElement(vdom, container, oldDom); 34 | } 35 | } 36 | 37 | const mountElement = function (vdom, container, oldDom) { 38 | // Native DOM elements as well as functions. 39 | return mountSimpleNode(vdom, container, oldDom); 40 | } 41 | 42 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 43 | let newDomElement = null; 44 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 45 | 46 | if (vdom.type === "text") { 47 | newDomElement = document.createTextNode(vdom.props.textContent); 48 | } else { 49 | newDomElement = document.createElement(vdom.type); 50 | updateDomElement(newDomElement, vdom); 51 | } 52 | 53 | // Setting reference to vdom to dom 54 | newDomElement._virtualElement = vdom; 55 | if (nextSibling) { 56 | container.insertBefore(newDomElement, nextSibling); 57 | } else { 58 | container.appendChild(newDomElement); 59 | } 60 | 61 | //TODO: Render children 62 | vdom.children.forEach(child => { 63 | mountElement(child, newDomElement); 64 | }); 65 | 66 | } 67 | 68 | //TODO: Set DOM attributes and events 69 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 70 | const newProps = newVirtualElement.props || {}; 71 | const oldProps = oldVirtualElement.props || {}; 72 | Object.keys(newProps).forEach(propName => { 73 | const newProp = newProps[propName]; 74 | const oldProp = oldProps[propName]; 75 | if (newProp !== oldProp) { 76 | if (propName.slice(0, 2) === "on") { 77 | // prop is an event handler 78 | const eventName = propName.toLowerCase().slice(2); 79 | domElement.addEventListener(eventName, newProp, false); 80 | if (oldProp) { 81 | domElement.removeEventListener(eventName, oldProp, false); 82 | } 83 | } else if (propName === "value" || propName === "checked") { 84 | // this are special attributes that cannot be set 85 | // using setAttribute 86 | domElement[propName] = newProp; 87 | } else if (propName !== "children") { 88 | // ignore the 'children' prop 89 | if (propName === "className") { 90 | domElement.setAttribute("class", newProps[propName]); 91 | } else { 92 | domElement.setAttribute(propName, newProps[propName]); 93 | } 94 | } 95 | } 96 | }); 97 | // remove oldProps 98 | Object.keys(oldProps).forEach(propName => { 99 | const newProp = newProps[propName]; 100 | const oldProp = oldProps[propName]; 101 | if (!newProp) { 102 | if (propName.slice(0, 2) === "on") { 103 | // prop is an event handler 104 | domElement.removeEventListener(propName, oldProp, false); 105 | } else if (propName !== "children") { 106 | // ignore the 'children' prop 107 | domElement.removeAttribute(propName); 108 | } 109 | } 110 | }); 111 | } 112 | 113 | 114 | return { 115 | createElement, 116 | render 117 | } 118 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.7.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | 9 | const TinyReact = (function () { 10 | function createElement(type, attributes = {}, ...children) { 11 | let childElements = [].concat(...children).reduce( 12 | (acc, child) => { 13 | if (child != null && child !== true && child !== false) { 14 | if (child instanceof Object) { 15 | acc.push(child); 16 | } else { 17 | acc.push(createElement("text", { 18 | textContent: child 19 | })); 20 | } 21 | } 22 | return acc; 23 | } 24 | , []); 25 | return { 26 | type, 27 | children: childElements, 28 | props: Object.assign({ children: childElements }, attributes) 29 | } 30 | } 31 | 32 | const render = function (vdom, container, oldDom = container.firstChild) { 33 | diff(vdom, container, oldDom); 34 | } 35 | 36 | const diff = function (vdom, container, oldDom) { 37 | let oldvdom = oldDom && oldDom._virtualElement; 38 | 39 | if (!oldDom) { 40 | mountElement(vdom, container, oldDom); 41 | } 42 | else if (oldvdom && oldvdom.type === vdom.type) { 43 | if (oldvdom.type === "text") { 44 | updateTextNode(oldDom, vdom, oldvdom); 45 | } else { 46 | updateDomElement(oldDom, vdom, oldvdom); 47 | } 48 | 49 | // Set a reference to updated vdom 50 | oldDom._virtualElement = vdom; 51 | 52 | // Recursively diff children.. 53 | // Doing an index by index diffing (because we don't have keys yet) 54 | vdom.children.forEach((child, i) => { 55 | diff(child, oldDom, oldDom.childNodes[i]); 56 | }); 57 | 58 | } 59 | } 60 | 61 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 62 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 63 | domElement.textContent = newVirtualElement.props.textContent; 64 | } 65 | // Set a reference to the newvddom in oldDom 66 | domElement._virtualElement = newVirtualElement; 67 | } 68 | 69 | 70 | const mountElement = function (vdom, container, oldDom) { 71 | // Native DOM elements as well as functions. 72 | return mountSimpleNode(vdom, container, oldDom); 73 | } 74 | 75 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 76 | let newDomElement = null; 77 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 78 | 79 | if (vdom.type === "text") { 80 | newDomElement = document.createTextNode(vdom.props.textContent); 81 | } else { 82 | newDomElement = document.createElement(vdom.type); 83 | updateDomElement(newDomElement, vdom); 84 | } 85 | 86 | // Setting reference to vdom to dom 87 | newDomElement._virtualElement = vdom; 88 | if (nextSibling) { 89 | container.insertBefore(newDomElement, nextSibling); 90 | } else { 91 | container.appendChild(newDomElement); 92 | } 93 | 94 | //TODO: Render children 95 | vdom.children.forEach(child => { 96 | mountElement(child, newDomElement); 97 | }); 98 | 99 | } 100 | 101 | //TODO: Set DOM attributes and events 102 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 103 | const newProps = newVirtualElement.props || {}; 104 | const oldProps = oldVirtualElement.props || {}; 105 | Object.keys(newProps).forEach(propName => { 106 | const newProp = newProps[propName]; 107 | const oldProp = oldProps[propName]; 108 | if (newProp !== oldProp) { 109 | if (propName.slice(0, 2) === "on") { 110 | // prop is an event handler 111 | const eventName = propName.toLowerCase().slice(2); 112 | domElement.addEventListener(eventName, newProp, false); 113 | if (oldProp) { 114 | domElement.removeEventListener(eventName, oldProp, false); 115 | } 116 | } else if (propName === "value" || propName === "checked") { 117 | // this are special attributes that cannot be set 118 | // using setAttribute 119 | domElement[propName] = newProp; 120 | } else if (propName !== "children") { 121 | // ignore the 'children' prop 122 | if (propName === "className") { 123 | domElement.setAttribute("class", newProps[propName]); 124 | } else { 125 | domElement.setAttribute(propName, newProps[propName]); 126 | } 127 | } 128 | } 129 | }); 130 | // remove oldProps 131 | Object.keys(oldProps).forEach(propName => { 132 | const newProp = newProps[propName]; 133 | const oldProp = oldProps[propName]; 134 | if (!newProp) { 135 | if (propName.slice(0, 2) === "on") { 136 | // prop is an event handler 137 | domElement.removeEventListener(propName, oldProp, false); 138 | } else if (propName !== "children") { 139 | // ignore the 'children' prop 140 | domElement.removeAttribute(propName); 141 | } 142 | } 143 | }); 144 | } 145 | 146 | 147 | return { 148 | createElement, 149 | render 150 | } 151 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.8.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | 10 | const TinyReact = (function () { 11 | function createElement(type, attributes = {}, ...children) { 12 | let childElements = [].concat(...children).reduce( 13 | (acc, child) => { 14 | if (child != null && child !== true && child !== false) { 15 | if (child instanceof Object) { 16 | acc.push(child); 17 | } else { 18 | acc.push(createElement("text", { 19 | textContent: child 20 | })); 21 | } 22 | } 23 | return acc; 24 | } 25 | , []); 26 | return { 27 | type, 28 | children: childElements, 29 | props: Object.assign({ children: childElements }, attributes) 30 | } 31 | } 32 | 33 | const render = function (vdom, container, oldDom = container.firstChild) { 34 | diff(vdom, container, oldDom); 35 | } 36 | 37 | const diff = function (vdom, container, oldDom) { 38 | let oldvdom = oldDom && oldDom._virtualElement; 39 | 40 | if (!oldDom) { 41 | mountElement(vdom, container, oldDom); 42 | } 43 | else if (oldvdom && oldvdom.type === vdom.type) { 44 | if (oldvdom.type === "text") { 45 | updateTextNode(oldDom, vdom, oldvdom); 46 | } else { 47 | updateDomElement(oldDom, vdom, oldvdom); 48 | } 49 | 50 | // Set a reference to updated vdom 51 | oldDom._virtualElement = vdom; 52 | 53 | // Recursively diff children.. 54 | // Doing an index by index diffing (because we don't have keys yet) 55 | vdom.children.forEach((child, i) => { 56 | diff(child, oldDom, oldDom.childNodes[i]); 57 | }); 58 | 59 | // Remove old dom nodes 60 | let oldNodes = oldDom.childNodes; 61 | if (oldNodes.length > vdom.children.length) { 62 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 63 | let nodeToBeRemoved = oldNodes[i]; 64 | unmountNode(nodeToBeRemoved, oldDom); 65 | } 66 | } 67 | 68 | } 69 | } 70 | 71 | function unmountNode(domElement, parentComponent) { 72 | domElement.remove(); 73 | } 74 | 75 | 76 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 77 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 78 | domElement.textContent = newVirtualElement.props.textContent; 79 | } 80 | // Set a reference to the newvddom in oldDom 81 | domElement._virtualElement = newVirtualElement; 82 | } 83 | 84 | 85 | const mountElement = function (vdom, container, oldDom) { 86 | // Native DOM elements as well as functions. 87 | return mountSimpleNode(vdom, container, oldDom); 88 | } 89 | 90 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 91 | let newDomElement = null; 92 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 93 | 94 | if (vdom.type === "text") { 95 | newDomElement = document.createTextNode(vdom.props.textContent); 96 | } else { 97 | newDomElement = document.createElement(vdom.type); 98 | updateDomElement(newDomElement, vdom); 99 | } 100 | 101 | // Setting reference to vdom to dom 102 | newDomElement._virtualElement = vdom; 103 | if (nextSibling) { 104 | container.insertBefore(newDomElement, nextSibling); 105 | } else { 106 | container.appendChild(newDomElement); 107 | } 108 | 109 | //TODO: Render children 110 | vdom.children.forEach(child => { 111 | mountElement(child, newDomElement); 112 | }); 113 | 114 | } 115 | 116 | //TODO: Set DOM attributes and events 117 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 118 | const newProps = newVirtualElement.props || {}; 119 | const oldProps = oldVirtualElement.props || {}; 120 | Object.keys(newProps).forEach(propName => { 121 | const newProp = newProps[propName]; 122 | const oldProp = oldProps[propName]; 123 | if (newProp !== oldProp) { 124 | if (propName.slice(0, 2) === "on") { 125 | // prop is an event handler 126 | const eventName = propName.toLowerCase().slice(2); 127 | domElement.addEventListener(eventName, newProp, false); 128 | if (oldProp) { 129 | domElement.removeEventListener(eventName, oldProp, false); 130 | } 131 | } else if (propName === "value" || propName === "checked") { 132 | // this are special attributes that cannot be set 133 | // using setAttribute 134 | domElement[propName] = newProp; 135 | } else if (propName !== "children") { 136 | // ignore the 'children' prop 137 | if (propName === "className") { 138 | domElement.setAttribute("class", newProps[propName]); 139 | } else { 140 | domElement.setAttribute(propName, newProps[propName]); 141 | } 142 | } 143 | } 144 | }); 145 | // remove oldProps 146 | Object.keys(oldProps).forEach(propName => { 147 | const newProp = newProps[propName]; 148 | const oldProp = oldProps[propName]; 149 | if (!newProp) { 150 | if (propName.slice(0, 2) === "on") { 151 | // prop is an event handler 152 | domElement.removeEventListener(propName, oldProp, false); 153 | } else if (propName !== "children") { 154 | // ignore the 'children' prop 155 | domElement.removeAttribute(propName); 156 | } 157 | } 158 | }); 159 | } 160 | 161 | 162 | return { 163 | createElement, 164 | render 165 | } 166 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.9.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | 11 | const TinyReact = (function () { 12 | function createElement(type, attributes = {}, ...children) { 13 | let childElements = [].concat(...children).reduce( 14 | (acc, child) => { 15 | if (child != null && child !== true && child !== false) { 16 | if (child instanceof Object) { 17 | acc.push(child); 18 | } else { 19 | acc.push(createElement("text", { 20 | textContent: child 21 | })); 22 | } 23 | } 24 | return acc; 25 | } 26 | , []); 27 | return { 28 | type, 29 | children: childElements, 30 | props: Object.assign({ children: childElements }, attributes) 31 | } 32 | } 33 | 34 | const render = function (vdom, container, oldDom = container.firstChild) { 35 | diff(vdom, container, oldDom); 36 | } 37 | 38 | const diff = function (vdom, container, oldDom) { 39 | let oldvdom = oldDom && oldDom._virtualElement; 40 | 41 | if (!oldDom) { 42 | mountElement(vdom, container, oldDom); 43 | } 44 | else if (typeof vdom.type === "function") { 45 | diffComponent(vdom, null, container, oldDom); 46 | } 47 | else if (oldvdom && oldvdom.type === vdom.type) { 48 | if (oldvdom.type === "text") { 49 | updateTextNode(oldDom, vdom, oldvdom); 50 | } else { 51 | updateDomElement(oldDom, vdom, oldvdom); 52 | } 53 | 54 | // Set a reference to updated vdom 55 | oldDom._virtualElement = vdom; 56 | 57 | // Recursively diff children.. 58 | // Doing an index by index diffing (because we don't have keys yet) 59 | vdom.children.forEach((child, i) => { 60 | diff(child, oldDom, oldDom.childNodes[i]); 61 | }); 62 | 63 | // Remove old dom nodes 64 | let oldNodes = oldDom.childNodes; 65 | if (oldNodes.length > vdom.children.length) { 66 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 67 | let nodeToBeRemoved = oldNodes[i]; 68 | unmountNode(nodeToBeRemoved, oldDom); 69 | } 70 | } 71 | 72 | } 73 | } 74 | 75 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 76 | if (!oldComponent) { 77 | mountElement(newVirtualElement, container, domElement); 78 | } 79 | } 80 | 81 | const mountElement = function (vdom, container, oldDom) { 82 | if (isFunction(vdom)) { 83 | return mountComponent(vdom, container, oldDom); 84 | } else { 85 | return mountSimpleNode(vdom, container, oldDom); 86 | } 87 | } 88 | 89 | function isFunction(obj) { 90 | return obj && 'function' === typeof obj.type; 91 | } 92 | 93 | function isFunctionalComponent(vnode) { 94 | let nodeType = vnode && vnode.type; 95 | return nodeType && isFunction(vnode) 96 | && !(nodeType.prototype && nodeType.prototype.render); 97 | } 98 | 99 | function buildFunctionalComponent(vnode, context) { 100 | return vnode.type(); 101 | } 102 | 103 | 104 | function mountComponent(vdom, container, oldDomElement) { 105 | let nextvDom = null, component = null, newDomElement = null; 106 | if (isFunctionalComponent(vdom)) { 107 | nextvDom = buildFunctionalComponent(vdom); 108 | } 109 | 110 | // Recursively render child components 111 | if (isFunction(nextvDom)) { 112 | return mountComponent(nextvDom, container, oldDomElement); 113 | } else { 114 | newDomElement = mountElement(nextvDom, container, oldDomElement); 115 | } 116 | return newDomElement; 117 | 118 | } 119 | 120 | function unmountNode(domElement, parentComponent) { 121 | domElement.remove(); 122 | } 123 | 124 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 125 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 126 | domElement.textContent = newVirtualElement.props.textContent; 127 | } 128 | // Set a reference to the newvddom in oldDom 129 | domElement._virtualElement = newVirtualElement; 130 | } 131 | 132 | 133 | 134 | 135 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 136 | let newDomElement = null; 137 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 138 | 139 | if (vdom.type === "text") { 140 | newDomElement = document.createTextNode(vdom.props.textContent); 141 | } else { 142 | newDomElement = document.createElement(vdom.type); 143 | updateDomElement(newDomElement, vdom); 144 | } 145 | 146 | // Setting reference to vdom to dom 147 | newDomElement._virtualElement = vdom; 148 | if (nextSibling) { 149 | container.insertBefore(newDomElement, nextSibling); 150 | } else { 151 | container.appendChild(newDomElement); 152 | } 153 | 154 | //TODO: Render children 155 | vdom.children.forEach(child => { 156 | mountElement(child, newDomElement); 157 | }); 158 | 159 | } 160 | 161 | //TODO: Set DOM attributes and events 162 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 163 | const newProps = newVirtualElement.props || {}; 164 | const oldProps = oldVirtualElement.props || {}; 165 | Object.keys(newProps).forEach(propName => { 166 | const newProp = newProps[propName]; 167 | const oldProp = oldProps[propName]; 168 | if (newProp !== oldProp) { 169 | if (propName.slice(0, 2) === "on") { 170 | // prop is an event handler 171 | const eventName = propName.toLowerCase().slice(2); 172 | domElement.addEventListener(eventName, newProp, false); 173 | if (oldProp) { 174 | domElement.removeEventListener(eventName, oldProp, false); 175 | } 176 | } else if (propName === "value" || propName === "checked") { 177 | // this are special attributes that cannot be set 178 | // using setAttribute 179 | domElement[propName] = newProp; 180 | } else if (propName !== "children") { 181 | // ignore the 'children' prop 182 | if (propName === "className") { 183 | domElement.setAttribute("class", newProps[propName]); 184 | } else { 185 | domElement.setAttribute(propName, newProps[propName]); 186 | } 187 | } 188 | } 189 | }); 190 | // remove oldProps 191 | Object.keys(oldProps).forEach(propName => { 192 | const newProp = newProps[propName]; 193 | const oldProp = oldProps[propName]; 194 | if (!newProp) { 195 | if (propName.slice(0, 2) === "on") { 196 | // prop is an event handler 197 | domElement.removeEventListener(propName, oldProp, false); 198 | } else if (propName !== "children") { 199 | // ignore the 'children' prop 200 | domElement.removeAttribute(propName); 201 | } 202 | } 203 | }); 204 | } 205 | 206 | 207 | return { 208 | createElement, 209 | render 210 | } 211 | }()); -------------------------------------------------------------------------------- /lib/demo-step-by-step/dom.js: -------------------------------------------------------------------------------- 1 | // 1. createElement Stub 2 | // 2. createElement Basic Implementation 3 | // 3. createElement Handle true/false short circuiting 4 | // 4. createElement remove undefined nodes 5 | // 5. rendering native DOM elements along with children 6 | // 6. set dom attributes and events 7 | // 7. diffing two trees of native DOM elements 8 | // 8. removing extra nodes 9 | // 9. rendering functional component 10 | // 10. passing props to functional component 11 | // 11. functional component diffing (remove extra nodes) 12 | // 12. rendering stateful component 13 | // 13. passing props to stateful component 14 | // 14. implement setState() method -> parameter as object 15 | // 15. implement the stub for lifecycle method (TODO) 16 | // 16. diffing stateful components (ISSUE: new props coming as #text->object should be toString() in app) 17 | // 17. Adding support for ref (todo) 18 | // 18. More test cases, old and new types are different. 19 | // 19. Add support for passing style attributes as objects 20 | 21 | 22 | const TinyReact = (function () { 23 | function createElement(type, attributes = {}, ...children) { 24 | let childElements = [].concat(...children).reduce( 25 | (acc, child) => { 26 | if (child != null && child !== true && child !== false) { 27 | if (child instanceof Object) { 28 | acc.push(child); 29 | } else { 30 | acc.push(createElement("text", { 31 | textContent: child 32 | })); 33 | } 34 | } 35 | return acc; 36 | } 37 | , []); 38 | return { 39 | type, 40 | children: childElements, 41 | props: Object.assign({ children: childElements }, attributes) 42 | } 43 | } 44 | 45 | const render = function (vdom, container, oldDom = container.firstChild) { 46 | diff(vdom, container, oldDom); 47 | } 48 | 49 | const diff = function (vdom, container, oldDom) { 50 | let oldvdom = oldDom && oldDom._virtualElement; 51 | let oldComponent = oldvdom && oldvdom.component; 52 | 53 | if (!oldDom) { 54 | mountElement(vdom, container, oldDom); 55 | } 56 | else if ((vdom.type !== oldvdom.type) && (typeof vdom.type !== "function")) { 57 | let newDomElement = createDomElement(vdom); // todo: 58 | oldDom.parentNode.replaceChild(newDomElement, oldDom); 59 | } 60 | else if (typeof vdom.type === "function") { 61 | diffComponent(vdom, oldComponent, container, oldDom); 62 | } 63 | else if (oldvdom && oldvdom.type === vdom.type) { 64 | if (oldvdom.type === "text") { 65 | updateTextNode(oldDom, vdom, oldvdom); 66 | } else { 67 | updateDomElement(oldDom, vdom, oldvdom); 68 | } 69 | 70 | // Set a reference to updated vdom 71 | oldDom._virtualElement = vdom; 72 | 73 | // Lets create a collection of keyed elements 74 | let keyedElements = {}; 75 | for (let i = 0; i < oldDom.childNodes.length; i += 1) { 76 | const domElement = oldDom.childNodes[i]; 77 | const key = domElement._virtualElement.props.key; 78 | 79 | if (key) { 80 | keyedElements[key] = { 81 | domElement, 82 | index: i 83 | }; 84 | } 85 | 86 | } 87 | 88 | 89 | // Recursively diff children.. 90 | // Doing an index by index diffing (because we don't have keys yet) 91 | if (Object.keys(keyedElements).length === 0) { 92 | vdom.children.forEach((child, i) => { 93 | diff(child, oldDom, oldDom.childNodes[i]); 94 | }); 95 | } else { 96 | // Reconciliation based on keys 97 | vdom.children.forEach((virtualElement, i) => { 98 | const key = virtualElement.props.key; 99 | if (key) { 100 | const keyedDomElement = keyedElements[key]; 101 | if (keyedDomElement) { 102 | // Position the new element correctly based on key/index 103 | if (oldDom.childNodes[i] && !oldDom.childNodes[i].isSameNode(keyedDomElement.domElement)) { 104 | oldDom.insertBefore(keyedDomElement.domElement, 105 | oldDom.childNodes[i]); 106 | } 107 | diff(virtualElement, oldDom, keyedDomElement.domElement); 108 | } 109 | else { 110 | mountElement(virtualElement, oldDom); 111 | } 112 | } 113 | }); 114 | } 115 | 116 | // Remove old dom nodes 117 | let oldNodes = oldDom.childNodes; 118 | if (Object.keys(keyedElements).length === 0) { 119 | if (oldNodes.length > vdom.children.length) { 120 | for (let i = oldNodes.length - 1; i >= vdom.children.length; i -= 1) { 121 | let nodeToBeRemoved = oldNodes[i]; 122 | unmountNode(nodeToBeRemoved, oldDom); 123 | } 124 | } 125 | } else { 126 | if (oldNodes.length > vdom.children.length) { 127 | for (let i = 0; i < oldDom.childNodes.length; i += 1) { 128 | let oldChild = oldDom.childNodes[i]; 129 | let oldKey = oldChild.getAttribute("key"); 130 | 131 | let found = false; 132 | for (let n = 0; n < vdom.children.length; n += 1) { 133 | if (vdom.children[n].props.key == oldKey) { 134 | found = true; 135 | break; 136 | } 137 | } 138 | 139 | if (!found) { 140 | unmountNode(oldChild, oldDom); 141 | } 142 | 143 | } 144 | } 145 | } 146 | 147 | } 148 | } 149 | 150 | function createDomElement(vdom) { 151 | let newDomElement = null; 152 | if (vdom.type === "text") { 153 | newDomElement = document.createTextNode(vdom.props.textContent); 154 | } else { 155 | newDomElement = document.createElement(vdom.type); 156 | updateDomElement(newDomElement, vdom); 157 | } 158 | 159 | newDomElement._virtualElement = vdom; 160 | vdom.children.forEach((child) => { 161 | newDomElement.appendChild(createDomElement(child)); 162 | }); 163 | 164 | // Set refs 165 | if (vdom.props && vdom.props.ref) { 166 | vdom.props.ref(newDomElement); 167 | } 168 | 169 | return newDomElement; 170 | } 171 | 172 | function diffComponent(newVirtualElement, oldComponent, container, domElement) { 173 | 174 | if (isSameComponentType(oldComponent, newVirtualElement)) { 175 | updateComponent(newVirtualElement, oldComponent, container, domElement); 176 | } else { 177 | mountElement(newVirtualElement, container, domElement); 178 | } 179 | } 180 | 181 | function updateComponent(newVirtualElement, oldComponent, container, domElement) { 182 | 183 | // Lifecycle method 184 | oldComponent.componentWillReceiveProps(newVirtualElement.props); 185 | 186 | // Lifecycle method 187 | if (oldComponent.shouldComponentUpdate(newVirtualElement.props)) { 188 | const prevProps = oldComponent.props; 189 | 190 | // Invoke LifeCycle 191 | oldComponent.componentWillUpdate( 192 | newVirtualElement.props, 193 | oldComponent.state 194 | ); 195 | 196 | // Update component 197 | oldComponent.updateProps(newVirtualElement.props); 198 | 199 | // Call Render 200 | // Generate new vdom 201 | const nextElement = oldComponent.render(); 202 | nextElement.component = oldComponent; 203 | 204 | // Recursively diff again 205 | diff(nextElement, container, domElement, oldComponent); 206 | 207 | // Invoke LifeCycle 208 | oldComponent.componentDidUpdate(prevProps); 209 | 210 | } 211 | 212 | } 213 | 214 | function isSameComponentType(oldComponent, newVirtualElement) { 215 | return oldComponent && newVirtualElement.type === oldComponent.constructor; 216 | } 217 | 218 | const mountElement = function (vdom, container, oldDom) { 219 | if (isFunction(vdom)) { 220 | return mountComponent(vdom, container, oldDom); 221 | } else { 222 | return mountSimpleNode(vdom, container, oldDom); 223 | } 224 | } 225 | 226 | function isFunction(obj) { 227 | return obj && 'function' === typeof obj.type; 228 | } 229 | 230 | function isFunctionalComponent(vnode) { 231 | let nodeType = vnode && vnode.type; 232 | return nodeType && isFunction(vnode) 233 | && !(nodeType.prototype && nodeType.prototype.render); 234 | } 235 | 236 | function buildFunctionalComponent(vnode, context) { 237 | return vnode.type(vnode.props || {}); 238 | } 239 | 240 | 241 | function buildStatefulComponent(virtualElement) { 242 | const component = new virtualElement.type(virtualElement.props); 243 | const nextElement = component.render(); 244 | nextElement.component = component; 245 | return nextElement; 246 | } 247 | 248 | function mountComponent(vdom, container, oldDomElement) { 249 | let nextvDom = null, component = null, newDomElement = null; 250 | if (isFunctionalComponent(vdom)) { 251 | nextvDom = buildFunctionalComponent(vdom); 252 | } else { 253 | nextvDom = buildStatefulComponent(vdom); 254 | component = nextvDom.component; 255 | } 256 | 257 | // Recursively render child components 258 | if (isFunction(nextvDom)) { 259 | return mountComponent(nextvDom, container, oldDomElement); 260 | } else { 261 | newDomElement = mountElement(nextvDom, container, oldDomElement); 262 | } 263 | 264 | if (component) { 265 | component.componentDidMount(); // Life cycle method 266 | if (component.props.ref) { 267 | component.props.ref(component); 268 | } 269 | } 270 | 271 | return newDomElement; 272 | } 273 | 274 | function unmountNode(domElement, parentComponent) { 275 | const virtualElement = domElement._virtualElement; 276 | if (!virtualElement) { 277 | domElement.remove(); 278 | return; 279 | } 280 | 281 | // If component exist 282 | let oldComponent = domElement._virtualElement.component; 283 | if (oldComponent) { 284 | oldComponent.componentWillUnmount(); 285 | } 286 | 287 | // Recursive calls agains 288 | while (domElement.childNodes.length > 0) { 289 | unmountNode(domElement.firstChild); 290 | } 291 | 292 | if (virtualElement.props && virtualElement.props.ref) { 293 | virtualElement.props.ref(null); 294 | } 295 | 296 | // Clear out event handlers 297 | Object.keys(virtualElement.props).forEach(propName => { 298 | if (propName.slice(0, 2) === "on") { 299 | const event = propName.toLowerCase().slice(2); 300 | const handler = virtualElement.props[propName]; 301 | domElement.removeEventListener(event, handler); 302 | } 303 | }); 304 | 305 | domElement.remove(); 306 | } 307 | 308 | function updateTextNode(domElement, newVirtualElement, oldVirtualElement) { 309 | if (newVirtualElement.props.textContent !== oldVirtualElement.props.textContent) { 310 | domElement.textContent = newVirtualElement.props.textContent; 311 | } 312 | // Set a reference to the newvddom in oldDom 313 | domElement._virtualElement = newVirtualElement; 314 | } 315 | 316 | 317 | const mountSimpleNode = function (vdom, container, oldDomElement, parentComponent) { 318 | let newDomElement = null; 319 | const nextSibling = oldDomElement && oldDomElement.nextSibling; 320 | 321 | if (vdom.type === "text") { 322 | newDomElement = document.createTextNode(vdom.props.textContent); 323 | } else { 324 | newDomElement = document.createElement(vdom.type); 325 | updateDomElement(newDomElement, vdom); 326 | } 327 | 328 | // Setting reference to vdom to dom 329 | newDomElement._virtualElement = vdom; 330 | 331 | // TODO: Remove old nodes 332 | if (oldDomElement) { 333 | unmountNode(oldDomElement, parentComponent); 334 | } 335 | 336 | if (nextSibling) { 337 | container.insertBefore(newDomElement, nextSibling); 338 | } else { 339 | container.appendChild(newDomElement); 340 | } 341 | 342 | let component = vdom.component; 343 | if (component) { 344 | component.setDomElement(newDomElement); 345 | } 346 | 347 | vdom.children.forEach(child => { 348 | mountElement(child, newDomElement); 349 | }); 350 | 351 | if (vdom.props && vdom.props.ref) { 352 | vdom.props.ref(newDomElement); 353 | } 354 | 355 | } 356 | 357 | function updateDomElement(domElement, newVirtualElement, oldVirtualElement = {}) { 358 | const newProps = newVirtualElement.props || {}; 359 | const oldProps = oldVirtualElement.props || {}; 360 | Object.keys(newProps).forEach(propName => { 361 | const newProp = newProps[propName]; 362 | const oldProp = oldProps[propName]; 363 | if (newProp !== oldProp) { 364 | if (propName.slice(0, 2) === "on") { 365 | // prop is an event handler 366 | const eventName = propName.toLowerCase().slice(2); 367 | domElement.addEventListener(eventName, newProp, false); 368 | if (oldProp) { 369 | domElement.removeEventListener(eventName, oldProp, false); 370 | } 371 | } else if (propName === "value" || propName === "checked") { 372 | // this are special attributes that cannot be set 373 | // using setAttribute 374 | domElement[propName] = newProp; 375 | } else if (propName !== "children") { 376 | 377 | // ignore the 'children' prop 378 | if (propName === "className") { 379 | domElement.setAttribute("class", newProps[propName]); 380 | } else if (propName === "style" && !newProps[propName].substring) { 381 | let styleText = styleObjToCss(newProps[propName]); 382 | domElement.style = styleText; 383 | } 384 | else { 385 | domElement.setAttribute(propName, newProps[propName]); 386 | } 387 | } 388 | } 389 | }); 390 | // remove oldProps 391 | Object.keys(oldProps).forEach(propName => { 392 | const newProp = newProps[propName]; 393 | const oldProp = oldProps[propName]; 394 | if (!newProp) { 395 | if (propName.slice(0, 2) === "on") { 396 | // prop is an event handler 397 | domElement.removeEventListener(propName, oldProp, false); 398 | } else if (propName !== "children") { 399 | // ignore the 'children' prop 400 | domElement.removeAttribute(propName); 401 | } 402 | } 403 | }); 404 | } 405 | 406 | function styleObjToCss(styleObj) { 407 | // I am not checking for non-dimensional props here 408 | // Assuming the correct dimensional values are passed for e.g. 10px etc 409 | 410 | let styleCss = "", sep = ":", term = ";"; 411 | 412 | for (let prop in styleObj) { 413 | if (styleObj.hasOwnProperty(prop)) { 414 | let val = styleObj[prop]; 415 | styleCss += `${jsToCss(prop)} : ${val} ${term}`; 416 | } 417 | } 418 | 419 | return styleCss; 420 | 421 | } 422 | 423 | function jsToCss(s) { 424 | // OLd preact code base. 425 | let transformedText = s.replace(/([A-Z])/, '-$1').toLowerCase(); 426 | // borderBottom transform to border-bottom 427 | return transformedText; 428 | } 429 | 430 | class Component { 431 | constructor(props) { 432 | this.props = props; 433 | this.state = {}; 434 | this.prevState = {}; 435 | } 436 | 437 | setState(nextState) { 438 | if (!this.prevState) this.prevState = this.state; 439 | 440 | this.state = Object.assign({}, this.state, nextState); 441 | 442 | let dom = this.getDomElement(); 443 | let container = dom.parentNode; 444 | 445 | let newvdom = this.render(); 446 | 447 | // Recursively diff 448 | diff(newvdom, container, dom); 449 | 450 | } 451 | 452 | // Helper methods 453 | setDomElement(dom) { 454 | this._dom = dom; 455 | } 456 | 457 | getDomElement() { 458 | return this._dom; 459 | } 460 | 461 | updateProps(props) { 462 | this.props = props; 463 | } 464 | 465 | // Lifecycle methods 466 | componentWillMount() { } 467 | componentDidMount() { } 468 | componentWillReceiveProps(nextProps) { } 469 | 470 | shouldComponentUpdate(nextProps, nextState) { 471 | return nextProps != this.props || nextState != this.state; 472 | } 473 | 474 | componentWillUpdate(nextProps, nextState) { } 475 | 476 | componentDidUpdate(prevProps, prevState) { } 477 | 478 | componentWillUnmount() { } 479 | } 480 | 481 | 482 | return { 483 | createElement, 484 | render, 485 | Component 486 | } 487 | }()); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-your-own-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "react-mini.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "body-parser": "^1.18.3", 15 | "express": "^4.17.1" 16 | }, 17 | "devDependencies": { 18 | "nodemon": "^1.18.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | var port = 3333; 5 | 6 | app.use(express.static("./")); 7 | 8 | // Our first route 9 | app.get('/', function (req, res) { 10 | res.sendFile('./index.html'); 11 | }); 12 | 13 | // Listen to port 5000 14 | app.listen(port, function () { 15 | console.log(`Server listening on port ${port}!`); 16 | }); -------------------------------------------------------------------------------- /style/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 1.15rem; 3 | background: #edeff0; 4 | padding: 0; 5 | margin: 0; 6 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 7 | height: 100vh; 8 | margin: 5px; 9 | } 10 | 11 | h1 { 12 | font-size: 2.1em; 13 | margin-top: 5px; 14 | margin-bottom: 0; 15 | color: #384047; 16 | line-height: 1; 17 | text-align: center; 18 | } 19 | 20 | h6 { 21 | text-align: center; 22 | margin-top: 6px; 23 | } 24 | 25 | .top-menu { 26 | background-color: skyblue; 27 | padding: 10px; 28 | width: 100%; 29 | text-align: center; 30 | } 31 | 32 | .top-menu a { 33 | display: inline-block; 34 | padding: 5px; 35 | } 36 | 37 | .top-menu a:hover { 38 | background-color: yellow; 39 | } 40 | 41 | .container p { 42 | text-align: center; 43 | } 44 | 45 | .todo-input-container { 46 | text-align: center; 47 | } 48 | 49 | .container { 50 | width: 450px; 51 | box-sizing: border-box; 52 | background: white; 53 | padding: 20px; 54 | border-radius: 5px; 55 | box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1); 56 | margin: 40px auto 0; 57 | } 58 | 59 | .todos { 60 | padding: 12px; 61 | margin-bottom: 20px; 62 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.12); 63 | } 64 | 65 | li { 66 | list-style-type: disc; 67 | margin-bottom: 5px; 68 | display: flex; 69 | align-items: center; 70 | } 71 | 72 | .todo-actions { 73 | margin-left: auto; 74 | } 75 | 76 | input { 77 | margin-right: 7px; 78 | caret-color: blue; 79 | } 80 | 81 | .addItemInput { 82 | width: 14em; 83 | padding: 1px 1px; 84 | margin-right: 7px; 85 | height: 1.5em; 86 | font-size: 90%; 87 | } 88 | 89 | .editItemInput { 90 | width: 14em; 91 | height: 1.6em; 92 | } 93 | 94 | /* Buttons */ 95 | button { 96 | color: white; 97 | background: #508abc; 98 | border: solid 1px; 99 | border-color: rgba(0, 0, 0, 0.1); 100 | border-radius: 5px; 101 | cursor: pointer; 102 | font-size: 0.75em; 103 | padding: 0.5em 0.65em; 104 | } 105 | 106 | .btnDelete { 107 | background-color: lightcoral !important; 108 | } 109 | 110 | button+button { 111 | margin-left: 0.5em; 112 | } 113 | 114 | .container button+button { 115 | background: #768da3; 116 | } 117 | 118 | .container li button+button { 119 | background: #508abc; 120 | } 121 | 122 | .upButton { 123 | margin-left: auto; 124 | background: #52bab3; 125 | } 126 | 127 | .container li button:last-child { 128 | background: #768da3; 129 | } 130 | 131 | .strike { 132 | text-decoration: line-through; 133 | color: gray; 134 | } 135 | 136 | /*** */ 137 | .todos { 138 | user-select: none; 139 | } 140 | 141 | .todo-actions { 142 | display: inline-block; 143 | padding-left: 40px; 144 | } 145 | 146 | .todo-item { 147 | padding: 5px; 148 | margin-top: 5px; 149 | } 150 | 151 | .todo-item-completed { 152 | text-decoration: line-through; 153 | background-color: darkgreen; 154 | color: white; 155 | } -------------------------------------------------------------------------------- /style/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /style/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /style/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /style/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /style/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /style/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /style/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /style/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /style/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /style/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /style/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /style/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshpillai/udemy-student-create-your-own-react-fasttrack/f07078fe5caa0cad8458318f6afb47e2ab9f6e50/style/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /todo-app.js: -------------------------------------------------------------------------------- 1 | /** @jsx TinyReact.createElement */ 2 | 3 | /*** Step 1,2,3,4 - createElement */ 4 | 5 | const root = document.getElementById("root"); 6 | 7 | var Step1 = ( 8 |
9 |

Hello Tiny React!

10 |

(coding nirvana)

11 |
nested 1
nested 1.1
12 |

(OBSERVE: This will change)

13 | {2 == 1 &&
Render this if 2==1
} 14 | {2 == 2 &&
{2}
} 15 | This is a text 16 | 17 |

This will be deleted

18 | 2,3 19 |
20 | ); 21 | 22 | console.log(Step1); 23 | 24 | // Step 5, 6 25 | //TinyReact.render(Step1, root); 26 | 27 | var Step2 = ( 28 |
29 |

Hello Tiny React!

30 |

(coding nirvana)

31 |
nested 1
nested 1.1
32 |

(OBSERVE: I said it!!)

33 | {2 == 1 &&
Render this if 2==1
} 34 | {2 == 2 &&
{2}
} 35 | Something goes here... 36 | 37 |
38 | ); 39 | 40 | // setTimeout(() => { 41 | // alert("Re-rendering..."); 42 | // TinyReact.render(Step2, root); 43 | // }, 4000); 44 | 45 | // Functional Component 46 | const Heart = (props) => ; 47 | 48 | // function Heart() { 49 | // return ( 50 | // 51 | // ); 52 | // } 53 | 54 | 55 | //TinyReact.render(, root); 56 | 57 | const Button = (props) => ; 58 | 59 | 60 | // Testing functional components, props, nested components 61 | 62 | const Greeting = function (props) { 63 | return ( 64 |
65 |

Welcome {props.message}

66 | 67 |
68 | ); 69 | } 70 | 71 | //TinyReact.render(, root); 72 | 73 | // setTimeout(() => { 74 | // alert("Re-rendering..."); 75 | // TinyReact.render(, root); 76 | // }, 4000); 77 | 78 | // Stateful component 79 | class Alert extends TinyReact.Component { 80 | constructor(props) { 81 | super(props); 82 | this.state = { 83 | title: "Default title" 84 | }; 85 | 86 | this.changeTitle = this.changeTitle.bind(this); // Binding the context to instance of Alert 87 | } 88 | 89 | changeTitle() { 90 | this.setState({ title: new Date().toString() }); 91 | } 92 | 93 | render() { 94 | return ( 95 |
96 |

{this.state.title}

97 |
98 | {this.props.message} 99 |
100 | 101 |
102 | ); 103 | } 104 | } 105 | 106 | //TinyReact.render(, root); 107 | 108 | // Diffing / Reconciliation of two stateful components 109 | 110 | class Stateful extends TinyReact.Component { 111 | constructor(props) { 112 | super(props); 113 | console.log(props); 114 | } 115 | render() { 116 | return ( 117 |
118 |

{this.props.title.toString()}

119 | 120 |
121 | ); 122 | } 123 | } 124 | 125 | //TinyReact.render(, root); 126 | 127 | 128 | function update() { 129 | TinyReact.render(, root); 130 | } 131 | 132 | 133 | class WishList extends TinyReact.Component { 134 | constructor(props) { 135 | super(props); 136 | this.state = { 137 | wish: { 138 | title: "I wan't to be a programmer" 139 | } 140 | } 141 | this.update = this.update.bind(this); 142 | } 143 | 144 | update() { 145 | let newValue = this.inputWish.value; // here inputWish is the input DOM element 146 | let wish = Object.assign({}, this.state.wish); 147 | // this.state.width = newValue; // BAD PRACTICE as we are mutating the state 148 | 149 | wish.title = newValue; 150 | this.setState({ 151 | wish 152 | }); 153 | } 154 | 155 | render() { 156 | return ( 157 |
158 |

Your wish list

159 | { this.inputWish = inputWish }} placeholder="What's your wish?"> 160 | 161 | 162 |
163 | {this.state.wish.title} 164 |
165 |
166 | ); 167 | } 168 | 169 | } 170 | 171 | //TinyReact.render(, root); 172 | 173 | let newElement = ( 174 |
175 |

One

176 |

Two

177 |
178 | ); 179 | 180 | //TinyReact.render(, root); 181 | 182 | // TinyReact.render(newElement, root); 183 | 184 | // setTimeout(() => { 185 | // alert("Rerendering"); 186 | // TinyReact.render(, root); 187 | // //TinyReact.render(newElement, root); 188 | // }, 4000); 189 | 190 | 191 | //////******* TODO APP ***********/ 192 | let Header = props => { 193 | return ( 194 |
195 |

{props.text}

196 |
(double click on todo to mark as completed)
197 |
198 | ); 199 | }; 200 | 201 | 202 | class TodoItem extends TinyReact.Component { 203 | constructor(props) { 204 | super(props); 205 | this.logging = true; 206 | } 207 | 208 | log(...args) { 209 | if (this.logging) { 210 | for (let i = 0; i < args.length; i++) { 211 | console.log(args[i]); 212 | } 213 | } 214 | } 215 | componentDidMount() { 216 | this.log("2. TodoItem:cdm"); 217 | } 218 | componentWillMount() { 219 | this.log("1. TodoItem:cwm"); 220 | } 221 | 222 | // VERY IMPORTANT 223 | shouldComponentUpdate(nextProps, nextState) { 224 | let result = nextProps.task != this.props.task; 225 | return result; 226 | } 227 | 228 | componentWillReceiveProps(nextProps) { 229 | this.log("TodoItem:cwrp: ", JSON.stringify(nextProps)); 230 | } 231 | componentWillUnmount() { 232 | this.log("TodoItem:cwu: " + this.props.task.title); 233 | } 234 | 235 | handleEdit = task => { 236 | this.props.onUpdateTask(task.id, this.textInput.value); 237 | }; 238 | 239 | editView = props => { 240 | if (props.task.edit) { 241 | return ( 242 | 243 | (this.textInput = input)} 248 | /> 249 | 255 | 256 | ); 257 | } 258 | return props.task.title; 259 | }; 260 | 261 | render() { 262 | let className = "todo-item "; 263 | if (this.props.task.completed) { 264 | className += "strike"; 265 | } 266 | 267 | let todoItemStyle = { 268 | borderBottom: "1px dashed gray", 269 | color: "red" 270 | }; 271 | 272 | return ( 273 |
  • this.props.onToggleComplete(this.props.task)} 278 | > 279 | {this.editView(this.props)} 280 |
    281 | 287 | 294 |
    295 |
  • 296 | ); 297 | } 298 | } 299 | 300 | class TodoApp extends TinyReact.Component { 301 | constructor(props) { 302 | super(props); 303 | this.addTodo = this.addTodo.bind(this); 304 | this.deleteTodo = this.deleteTodo.bind(this); 305 | this.onToggleEdit = this.onToggleEdit.bind(this); 306 | this.onUpdateTask = this.onUpdateTask.bind(this); 307 | this.onToggleComplete = this.onToggleComplete.bind(this); 308 | this.onKeyDown = this.onKeyDown.bind(this); 309 | 310 | this.state = { 311 | tasks: [{ id: 1, title: "Task 1", edit: false }], 312 | sortOrder: "asc" 313 | }; 314 | } 315 | 316 | onKeyDown(e) { 317 | if (e.which === 13) { 318 | this.addTodo(); 319 | } 320 | } 321 | deleteTodo(task) { 322 | var tasks = this.state.tasks.filter(t => { 323 | return t.id != task.id; 324 | }); 325 | 326 | this.setState({ 327 | header: "# Todos: " + tasks.length, 328 | tasks 329 | }); 330 | } 331 | 332 | addTodo() { 333 | if (this.newTodo.value.trim() == "") { 334 | alert("You don't wanna do anything !"); 335 | return; 336 | } 337 | let newTodo = { 338 | id: +new Date(), 339 | title: this.newTodo.value, 340 | edit: false 341 | }; 342 | this.setState({ 343 | tasks: [...this.state.tasks, newTodo] 344 | }); 345 | 346 | this.newTodo.value = ""; 347 | this.newTodo.focus(); 348 | } 349 | 350 | sortToDo = () => { 351 | let tasks = null; 352 | let sortOrder = this.state.sortOrder; 353 | if (!sortOrder) { 354 | tasks = this.state.tasks.sort( 355 | (a, b) => +(a.title > b.title) || -(a.title < b.title) 356 | ); 357 | sortOrder = "asc"; 358 | } else if (sortOrder === "asc") { 359 | sortOrder = "desc"; 360 | tasks = this.state.tasks.sort( 361 | (a, b) => +(b.title > a.title) || -(b.title < a.title) 362 | ); 363 | } else { 364 | sortOrder = "asc"; 365 | tasks = this.state.tasks.sort( 366 | (a, b) => +(a.title > b.title) || -(a.title < b.title) 367 | ); 368 | } 369 | this.setState({ 370 | tasks, 371 | sortOrder 372 | }); 373 | }; 374 | 375 | onUpdateTask(taskId, newTitle) { 376 | var tasks = this.state.tasks.map(t => { 377 | return t.id !== taskId ? 378 | t : 379 | Object.assign({}, t, { title: newTitle, edit: !t.edit }); 380 | }); 381 | 382 | this.setState({ 383 | tasks 384 | }); 385 | } 386 | 387 | // Uses setstate with fn argument 388 | onToggleEdit(task) { 389 | let tasks = this.state.tasks.map(t => { 390 | return t.id !== task.id ? 391 | t : 392 | Object.assign({}, t, { edit: !t.edit }); 393 | }); 394 | 395 | // DONT MUTATE STATE DIRECTLY 396 | // let tasks = this.state.tasks.map(t => { 397 | // if (t.id === task.id) { 398 | // t.edit = !t.edit; 399 | // } 400 | // return t; 401 | // }); 402 | 403 | this.setState({ 404 | tasks 405 | }); 406 | } 407 | 408 | onToggleComplete(task) { 409 | let tasks = this.state.tasks.map(t => { 410 | return t.id !== task.id ? 411 | t : 412 | Object.assign({}, t, { completed: !t.completed }); 413 | }); 414 | 415 | this.setState({ 416 | tasks 417 | }); 418 | } 419 | 420 | render() { 421 | let tasksUI = this.state.tasks.map((task, index) => { 422 | return ( 423 | 432 | ); 433 | }); 434 | 435 | let sortIcon = ; 436 | if (this.state.sortOrder === "asc") { 437 | sortIcon = ; 438 | } else { 439 | sortIcon = ; 440 | } 441 | 442 | return ( 443 |
    444 |
    445 | 446 |
    447 | (this.newTodo = newTodo)} 452 | placeholder="what do you want to do today?" 453 | /> 454 | 462 | 465 |
    466 |
      {tasksUI}
    467 |
    468 | ); 469 | } 470 | } 471 | 472 | TinyReact.render(, root); --------------------------------------------------------------------------------