UNPKG

18.8 kBJavaScriptView Raw
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
25var window = typeof window !== "undefined" && window || {};
26module.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})()