1 | /**
|
2 | * .dom - A Tiny VDom Template Engine
|
3 | *
|
4 | * Copyright 2017-2019 Ioannis Charalampidis (wavesoft)
|
5 | *
|
6 | * Licensed under the Apache License, Version 2.0 (the "License");
|
7 | * you may not use this file except in compliance with the License.
|
8 | * You may obtain a copy of the License at
|
9 | *
|
10 | * http://www.apache.org/licenses/LICENSE-2.0
|
11 | *
|
12 | * Unless required by applicable law or agreed to in writing, software
|
13 | * distributed under the License is distributed on an "AS IS" BASIS,
|
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15 | * See the License for the specific language governing permissions and
|
16 | * limitations under the License.
|
17 | */
|
18 |
|
19 | /* BEGIN NPM-GLUE */
|
20 |
|
21 | // This code block will be striped when building the stand-alone version.
|
22 | // When using `npm` this exports the correct functions in order to be easily
|
23 | // imported in the correct scope, without leaking to the global scope.
|
24 |
|
25 | var window = typeof window !== "undefined" && window || {};
|
26 | module.exports = window;
|
27 |
|
28 | /* END NPM-GLUE */
|
29 |
|
30 | (() => {
|
31 | let
|
32 |
|
33 | /**
|
34 | * Create a VNode element
|
35 | *
|
36 | * @param {String|Function} element - The tag name or the component to render
|
37 | * @param {Object} [props] - The object properties
|
38 | * @param {Array} [children] - The child VNode elements
|
39 | * @returns {VNode} Returns a virtual DOM instance
|
40 | */
|
41 | createElement = (element, props={}, ...children) => (
|
42 | {
|
43 |
|
44 | $: element, // '$' holds the name or function passed as
|
45 | // first argument
|
46 |
|
47 | a: (!props || props.$ || props.concat) // If the props argument is false/null, a renderable
|
48 | // VNode, a string, an array (.concat exists on both
|
49 | // strings and arrays), or a DOM element, then ...
|
50 |
|
51 | ? {c: [].concat(props || [], ...children)} // ... create props just with children
|
52 | : ((props.c = [].concat(...children)), props) // ... otherwise append 'c' to the property set
|
53 | // (the .concat ensures that arrays of children
|
54 | // will be flattened into a single array).
|
55 | }
|
56 | )
|
57 |
|
58 | /**
|
59 | * Helper method that calls all methods in an array of functions
|
60 | *
|
61 | * @param {Array} methods - The array of methods to call
|
62 | * @param {Object} arg1 - An arbitrary first argument
|
63 | * @param {Object} arg2 - An arbitrary second argument
|
64 | */
|
65 | , callLifecycleMethods = (methods = [], arg1, arg2) =>
|
66 | methods.map(e => e(arg1, arg2)) // Fan-out to the lifecycle methods, passing the
|
67 | // maximum of 2 arguments (spread operator takes
|
68 | // more space when compressed)
|
69 |
|
70 | /**
|
71 | * Helper function that wraps an element shorthand function with a proxy
|
72 | * that can be used to append class names to the instance.
|
73 | *
|
74 | * The result is wrapped with the same function, creating a chainable mechanism
|
75 | * for appending classes.
|
76 | *
|
77 | * @param {function} factoryFn - The factory function to call for creating vnode
|
78 | */
|
79 | , wrapClassProxy = factoryFn =>
|
80 | new Proxy( // We are creating a proxy object for every tag in
|
81 | // order to be able to customize the class name
|
82 | // via a shorthand call.
|
83 | factoryFn,
|
84 | {
|
85 | get: (targetFn, className, _instance) =>
|
86 | wrapClassProxy(
|
87 | (...args) => (
|
88 | (_instance=targetFn(...args)) // We first create the Virtual DOM instance by
|
89 | // calling the wrapped factory function
|
90 |
|
91 | .a.className = (_instance.a.className || ' ') // And then we assign the class name,
|
92 | + ' ' + className, // concatenating to the previous value
|
93 |
|
94 | _instance // And finally we return the instance
|
95 | )
|
96 | )
|
97 | }
|
98 | )
|
99 |
|
100 | /**
|
101 | * Render a VNode in the DOM
|
102 | *
|
103 | * @param {VNode|Array<VNode>} vnodes - The node on an array of nodes to render
|
104 | * @param {HTMLDomElement}
|
105 | */
|
106 | , render = window.R = (
|
107 | vnodes, // 1. The vnode tree to render
|
108 | dom, // 2. The DOMElement where to render into
|
109 |
|
110 | _children=dom.childNodes, // a. Shorthand for accessing the children
|
111 | _c=0 // b. Counter for processed children
|
112 | ) => {
|
113 |
|
114 | [].concat(vnodes).map( // Cast `vnodes` to array if nor already
|
115 |
|
116 | // In this `map` loop we ensure that the DOM
|
117 | // elements correspond to the correct virtual
|
118 | // node elements.
|
119 | (
|
120 | vnode, // 1. We handle the vnode from the array
|
121 | _new_dom, // 2. We ignore the index, and instead we are using
|
122 | // it as the new DOM element placeholder
|
123 | _callMethod, // 3. We ignore the array, and instead we are using
|
124 | // it as a variable where we are keeping the
|
125 | // lifecycle method to call at the end.
|
126 | _child=_children[_c++], // a. Get the next DOM child + increment counter
|
127 | _state=vnode.s=( // b. Get the current state from the DOM child and keep
|
128 | // a copy in the vnode object.
|
129 | _child // Separate comparison logic if there is a child or not
|
130 | ? ((_child.a == vnode.$) // - If the element has changed, bail
|
131 | && (vnode.s // - If there is a state in the VNode, prefer it
|
132 | || _child.s)) // - If there is a state in the DOM node, fall back to it
|
133 | : vnode.s // - If there is no element, use VNode state, if present
|
134 | ) || {}, // - Default state value
|
135 | _hooks={ // c. Prepare the hooks object that will be passed
|
136 | // down to the functional component
|
137 | a: vnode.$, // - The 'a' property is keeping a reference
|
138 | // to the element (property '$') and is used
|
139 | // for space-optimal assignment of the tag to
|
140 | // the DOM element through Object.assign in the
|
141 | // Update Element phase later.
|
142 | s: _state, // - The 's' property is keeping a reference to
|
143 | // the current element state. (Used above)
|
144 | m: [], // - The 'm' property contains the `mount` cb
|
145 | u: [], // - The 'u' property contains the `unmount` cb
|
146 | d: [] // - The 'd' property contains the `update` cb
|
147 | }
|
148 |
|
149 | ) => {
|
150 |
|
151 | /* Expand functional Components */
|
152 |
|
153 | for (;(vnode.$ || vnodes).bind;) // Expand recursive functional components until
|
154 | // we encounter a non-callable element. (The `vnodes` is
|
155 | // used as a reference to any kind of an object in order
|
156 | // to be able to resolve `.bind`, even if it's undefined).
|
157 | vnode = vnode.$(
|
158 |
|
159 | vnode.a, // 1. The component properties
|
160 | _state, // 2. The stateful component state
|
161 |
|
162 | (newState) => // 3. The setState function
|
163 |
|
164 | Object.assign( // First we update the state record, that also
|
165 | _state, // updates the contents of the DOM element, since
|
166 | newState // the reference is perserved.
|
167 | ) &&
|
168 | render( // We then trigger the same render cycle that will
|
169 | vnodes, // update the DOM
|
170 | dom
|
171 | ),
|
172 |
|
173 | _hooks // 4. The lifecycle method hooks
|
174 |
|
175 | );
|
176 |
|
177 | /* Create new DOM element */
|
178 | _new_dom =
|
179 | vnode.replace // in order to save a few comparison bytes later.
|
180 | ? document.createTextNode(vnode)
|
181 | : document.createElement(vnode.$);
|
182 |
|
183 | /* Keep or replace the previous DOM element */
|
184 |
|
185 | _new_dom =
|
186 | _child // If we have a previous child, do some reconciliation
|
187 | ? (_child.$ != vnode.$ && _child.data != vnode) // Check if the node tag has changed.
|
188 | ? (
|
189 | dom.replaceChild( // - If not, we replace the old element with the
|
190 | _new_dom, // new one.
|
191 | _child
|
192 | ),
|
193 | _new_dom // ... and we make sure we return the new DOM
|
194 | )
|
195 | : _child // - If it's the same, we keep the old child
|
196 |
|
197 | : dom.appendChild( // If we did not have a previous child, just
|
198 | _new_dom // append the new node
|
199 | )
|
200 |
|
201 | /* Prepare lifecycle methods */
|
202 |
|
203 | _callMethod = // We are not calling the method until we are done with
|
204 | // the rendering cycle. Otherwise this could cause an
|
205 | // infinite loop if `setState` is used.
|
206 |
|
207 | _child // If there is a DOM reflection
|
208 | ? _child.a == _hooks.a // .. and the element has not changed
|
209 | ? _hooks.d // - [D] Just update
|
210 | : (
|
211 | callLifecycleMethods(_child.u), // - [U] Otherwise unmount the previous one
|
212 | render( // And call the render function with empty
|
213 | [], // children in order to recursively unmount
|
214 | _child // the children tree.
|
215 | ),
|
216 | _hooks.m // - [M] Mount the new
|
217 | )
|
218 |
|
219 | // If there is no DOM reflection
|
220 | : _hooks.m; // - [M] Mount the new
|
221 |
|
222 | /* Update Element State */
|
223 |
|
224 | Object.assign(_new_dom, vnode, _hooks); // Keep the following information in the DOM:
|
225 | // - $ : The tag name from the vnode. We use this
|
226 | // instead of the .tagName because some
|
227 | // browsers convert it to capital-case
|
228 | // - u : The `didUnmount` hook that is called when
|
229 | // the DOM element is removed
|
230 | //
|
231 | // By assigning the entire _hooks and vnode
|
232 | // objects we expose some unneeded properties, but
|
233 | // it occupies less space than assigning $ and u
|
234 | // individually.
|
235 |
|
236 | /* Apply properties to the DOM element */
|
237 | vnode.replace
|
238 | ? _new_dom.data = vnode // - String nodes update only the text
|
239 | : Object.keys(vnode.a).map( // - Element nodes have properties
|
240 | (
|
241 | key // 1. The property name
|
242 | ) =>
|
243 |
|
244 | key == 'style' ? // The 'style' property is an object and must be
|
245 | // applied recursively.
|
246 | Object.assign(
|
247 | _new_dom[key], // '[key]' is shorter than '.style'
|
248 | vnode.a[key]
|
249 | )
|
250 |
|
251 | : (_new_dom[key] !== vnode.a[key] && // All properties are applied directly to DOM, as
|
252 | (_new_dom[key] = vnode.a[key])) // long as they are different than ther value in the
|
253 | // instance. This includes `onXXX` event handlers.
|
254 |
|
255 | ) && _hooks.r || // If the user has marked this element as 'raw', do not
|
256 | // continue to it's children. Failing to do so, will damage
|
257 | // the element contents
|
258 |
|
259 | render( // Only if we have an element (and not text node)
|
260 | vnode.a.c, // we recursively continue rendering into it's
|
261 | _new_dom // child nodes.
|
262 | )
|
263 |
|
264 | /* Call life-cycle methods */
|
265 |
|
266 | callLifecycleMethods(
|
267 | _callMethod,
|
268 | // Pass the following arguments:
|
269 | _new_dom, // 1. The new DOM instance
|
270 | _child // 2. The old DOM instance
|
271 | );
|
272 |
|
273 | }
|
274 | );
|
275 |
|
276 | /* Remove extraneous nodes */
|
277 |
|
278 | for (;_children[_c];) { // The _c property keeps track of the number of
|
279 | // elements in the VDom. If there are more child
|
280 | // nodes in the DOM, we remove them.
|
281 |
|
282 | callLifecycleMethods(_children[_c].u) // We then call the unmount lifecycle method for the
|
283 | // elements that will be removed
|
284 |
|
285 | render( // Remove child an trigger a recursive child removal
|
286 | [], // in order to call the correct lifecycle methods in our
|
287 | dom.removeChild(_children[_c]) // deep children too.
|
288 | )
|
289 |
|
290 | }
|
291 | }
|
292 |
|
293 | /**
|
294 | * Expose as `H` a proxy around the createElement function that can either be used
|
295 | * either as a function (ex. `H('div')`, or as a proxied method `H.div()` for creating
|
296 | * virtual DOM elements.
|
297 | */
|
298 | window.H = new Proxy(
|
299 | createElement,
|
300 | {
|
301 | get: (targetFn, tagName) =>
|
302 | targetFn[tagName] || // Make sure we don't override any native
|
303 | // property or method from the base function
|
304 |
|
305 | wrapClassProxy( // Otherwise, for every tag we extract a
|
306 | createElement.bind(targetFn, tagName) // class-wrapped crateElement method, bound to the
|
307 | ) // tag named as the property requested. We are not
|
308 | // using 'this', therefore we are using any reference
|
309 | } // that could lead on reduced code footprint.
|
310 | )
|
311 | })()
|