UNPKG

14 kBJavaScriptView Raw
1const EMPTY = {};
2const NO_RENDER = { render: false };
3const SYNC_RENDER = { renderSync: true };
4const DOM_RENDER = { build: true };
5const NON_DIMENSION_PROPS = `
6 boxFlex boxFlexGroup columnCount fillOpacity flex flexGrow
7 flexPositive flexShrink flexNegative fontWeight lineClamp
8 lineHeight opacity order orphans strokeOpacity widows zIndex zoom
9`.trim().split(/\s+/g).reduce( (acc, prop) => (acc[prop] = true, acc), {});
10
11let slice = Array.prototype.slice,
12 options = {
13 syncComponentUpdates: true
14 },
15 hooks = {};
16
17export { options, hooks };
18
19
20export function render(component, parent) {
21 let built = build(null, component),
22 c = built._component;
23 if (c) hook(c, 'componentWillMount');
24 parent.appendChild(built);
25 if (c) hook(c, 'componentDidMount');
26 return build;
27}
28
29
30hooks.vnode = ({ attributes }) => {
31 if (!attributes) return;
32
33 let s = attributes.style;
34 if (s && !s.substring) {
35 attributes.style = styleObjToCss(s);
36 }
37
38 let c = attributes['class'];
39 if (attributes.hasOwnProperty('className')) {
40 c = attributes['class'] = attributes.className;
41 delete attributes.className;
42 }
43 if (c && !c.substring) {
44 attributes['class'] = hashToClassName(c);
45 }
46};
47
48function styleObjToCss(s) {
49 let str = '',
50 sep = ': ',
51 term = '; ';
52 for (let prop in s) {
53 if (s.hasOwnProperty(prop)) {
54 let val = s[prop];
55 str += jsToCss(prop);
56 str += sep;
57 str += val;
58 if (typeof val==='number' && !NON_DIMENSION_PROPS.hasOwnProperty(prop)) {
59 str += 'px';
60 }
61 str += term;
62 }
63 }
64 return str;
65}
66
67function hashToClassName(c) {
68 let str = '';
69 for (let prop in c) {
70 if (c[prop]) {
71 if (str) str += ' ';
72 str += prop;
73 }
74 }
75 return str;
76}
77
78let jsToCss = s => s.replace(/([A-Z])/,'-$1').toLowerCase();
79
80
81
82/** Provides a base Component class with an API similar to React. */
83export class Component {
84 constructor() {
85 this._dirty = false;
86 this.props = hook(this, 'getDefaultProps') || {};
87 this.state = hook(this, 'getInitialState') || {};
88 hook(this, 'initialize');
89 }
90
91 shouldComponentUpdate(props, state) {
92 return true;
93 }
94
95 setState(state) {
96 extend(this.state, state);
97 this.triggerRender();
98 }
99
100 setProps(props, opts=EMPTY) {
101 let d = this._disableRendering===true;
102 this._disableRendering = true;
103 hook(this, 'componentWillReceiveProps', props, this.props);
104 //this.props = props;
105 this.nextProps = props;
106 this._disableRendering = d;
107 if (opts.renderSync===true && options.syncComponentUpdates===true) {
108 this._render();
109 }
110 else if (opts.render!==false) {
111 this.triggerRender();
112 }
113 }
114
115 triggerRender() {
116 if (this._dirty!==true) {
117 this._dirty = true;
118 renderQueue.add(this);
119 }
120 }
121
122 render(props, state) {
123 return h('div', { component:this.constructor.name }, props.children);
124 }
125
126 _render(opts=EMPTY) {
127 if (this._disableRendering===true) return;
128
129 this._dirty = false;
130
131 if (this.base && hook(this, 'shouldComponentUpdate', this.props, this.state)===false) {
132 this.props = this.nextProps;
133 return;
134 }
135
136 this.props = this.nextProps;
137
138 hook(this, 'componentWillUpdate');
139
140 let rendered = hook(this, 'render', this.props, this.state);
141
142 if (this.base || opts.build===true) {
143 let base = build(this.base, rendered);
144 if (this.base && base!==this.base) {
145 this.base.parentNode.insertBefore(base, this.base);
146 this.base.parentNode.removeChild(this.base);
147 }
148 this.base = base;
149 }
150
151 hook(this, 'componentDidUpdate');
152 }
153}
154
155
156
157/** jsx hyperscript generator
158 * To use, add the directive:
159 * /** @jsx h *\/
160 * import { render, h } from 'react-compat';
161 * render(<span>foo</span>, document.body);
162 */
163export function h(nodeName, attributes, ...args) {
164 let children = null,
165 sharedArr = [],
166 arr, lastSimple;
167 if (args.length) {
168 children = [];
169 for (let i=0; i<args.length; i++) {
170 if (Array.isArray(args[i])) {
171 arr = args[i];
172 }
173 else {
174 arr = sharedArr;
175 arr[0] = args[i];
176 }
177 for (let j=0; j<arr.length; j++) {
178 let child = arr[j];
179 let simple = notEmpty(child) && !isVNode(child);
180 if (simple) child = String(child);
181 if (simple && lastSimple) {
182 children[children.length-1] += child;
183 }
184 else if (child!==null && child!==undefined) {
185 children.push(child);
186 }
187 lastSimple = simple;
188 }
189 }
190 }
191
192 let p = new VNode(nodeName, attributes, children);
193 hook(hooks, 'vnode', p);
194 return p;
195}
196
197class VNode {
198 constructor(nodeName, attributes, children) {
199 this.nodeName = nodeName;
200 this.attributes = attributes;
201 this.children = children;
202 }
203}
204VNode.prototype.__isVNode = true;
205
206
207
208
209/** invoke a hook method gracefully */
210function hook(obj, name, ...args) {
211 let fn = obj[name];
212 if (fn && typeof fn==='function') return fn.apply(obj, args);
213}
214
215function isVNode(obj) {
216 return obj && obj.__isVNode===true;
217}
218
219function notEmpty(x) {
220 return x!==null && x!==undefined;
221}
222
223function isSameNodeType(node, vnode) {
224 if (node.nodeType===3) {
225 return typeof vnode==='string';
226 }
227 let nodeName = vnode.nodeName;
228 if (typeof nodeName==='function') return node._componentConstructor===nodeName;
229 return node.nodeName.toLowerCase()===nodeName;
230}
231
232
233function buildComponentFromVNode(dom, vnode) {
234 let c = dom && dom._component;
235
236 if (c && dom._componentConstructor===vnode.nodeName) {
237 let props = getNodeProps(vnode);
238 c.setProps(props, SYNC_RENDER);
239 return dom;
240 }
241 else {
242 if (c) unmountComponent(dom, c);
243 return createComponentFromVNode(vnode)
244 }
245}
246
247function createComponentFromVNode(vnode) {
248 let component = componentRecycler.create(vnode.nodeName);
249
250 let props = getNodeProps(vnode);
251 component.setProps(props, NO_RENDER);
252 component._render(DOM_RENDER);
253
254 let node = component.base;
255 node._component = component;
256 node._componentConstructor = vnode.nodeName;
257 return node;
258}
259
260function unmountComponent(dom, component) {
261 console.warn('unmounting mismatched component', component);
262
263 delete dom._component;
264 hook(component, 'componentWillUnmount');
265 let base = component.base;
266 if (base && base.parentNode) {
267 base.parentNode.removeChild(base);
268 }
269 hook(component, 'componentDidUnmount');
270 componentRecycler.collect(component);
271}
272
273
274/** Apply differences in a given vnode (and it's deep children) to a real DOM Node. */
275function build(dom, vnode) {
276 let out = dom,
277 nodeName = vnode.nodeName;
278
279 if (typeof nodeName==='function') {
280 return buildComponentFromVNode(dom, vnode);
281 }
282
283 if (typeof vnode==='string') {
284 if (dom) {
285 if (dom.nodeType===3) {
286 dom.textContent = vnode;
287 return dom;
288 }
289 else {
290 if (dom.nodeType===1) recycler.collect(dom);
291 }
292 }
293 return document.createTextNode(vnode);
294 }
295
296 if (!dom) {
297 out = recycler.create(nodeName);
298 }
299 else if (dom.nodeName.toLowerCase()!==nodeName) {
300 out = recycler.create(nodeName);
301 appendChildren(out, slice.call(dom.childNodes));
302 // reclaim element nodes
303 if (dom.nodeType===1) recycler.collect(dom);
304 }
305
306 // apply attributes
307 let old = getNodeAttributes(out) || EMPTY,
308 attrs = vnode.attributes || EMPTY;
309
310 // removed attributes
311 if (old!==EMPTY) {
312 for (let name in old) {
313 if (old.hasOwnProperty(name)) {
314 let o = attrs[name];
315 if (o===undefined || o===null || o===false) {
316 setAccessor(out, name, null, old[name]);
317 }
318 }
319 }
320 }
321
322 // new & updated attributes
323 if (attrs!==EMPTY) {
324 for (let name in attrs) {
325 if (attrs.hasOwnProperty(name)) {
326 let value = attrs[name];
327 if (value!==undefined && value!==null && value!==false) {
328 let prev = getAccessor(out, name, old[name]);
329 if (value!==prev) {
330 setAccessor(out, name, value, prev);
331 }
332 }
333 }
334 }
335 }
336
337
338 let children = slice.call(out.childNodes);
339 let keyed = {};
340 for (let i=children.length; i--; ) {
341 let t = children[i].nodeType;
342 let key;
343 if (t===3) {
344 key = t.key;
345 }
346 else if (t===1) {
347 key = children[i].getAttribute('key');
348 }
349 else {
350 continue;
351 }
352 if (key) keyed[key] = children.splice(i, 1)[0];
353 }
354 let newChildren = [];
355
356 if (vnode.children) {
357 for (let i=0, vlen=vnode.children.length; i<vlen; i++) {
358 let vchild = vnode.children[i];
359 let attrs = vchild.attributes;
360 let key, child;
361 if (attrs) {
362 key = attrs.key;
363 child = key && keyed[key];
364 }
365
366 // attempt to pluck a node of the same type from the existing children
367 if (!child) {
368 let len = children.length;
369 if (children.length) {
370 for (let j=0; j<len; j++) {
371 if (isSameNodeType(children[j], vchild)) {
372 child = children.splice(j, 1)[0];
373 break;
374 }
375 }
376 }
377 }
378
379 // morph the matched/found/created DOM child to match vchild (deep)
380 newChildren.push(build(child, vchild));
381 }
382 }
383
384 // apply the constructed/enhanced ordered list to the parent
385 for (let i=0, len=newChildren.length; i<len; i++) {
386 // we're intentionally re-referencing out.childNodes here as it is a live array (akin to live NodeList)
387 if (out.childNodes[i]!==newChildren[i]) {
388 let child = newChildren[i],
389 c = child._component,
390 next = out.childNodes[i+1];
391 if (c) hook(c, 'componentWillMount');
392 if (next) {
393 out.insertBefore(child, next);
394 }
395 else {
396 out.appendChild(child);
397 }
398 if (c) hook(c, 'componentDidMount');
399 }
400 }
401
402 // remove orphaned children
403 for (let i=0, len=children.length; i<len; i++) {
404 let child = children[i],
405 c = child._component;
406 if (c) hook(c, 'componentWillUnmount');
407 child.parentNode.removeChild(child);
408 if (c) {
409 hook(c, 'componentDidUnmount');
410 componentRecycler.collect(c);
411 }
412 else if (child.nodeType===1) {
413 recycler.collect(child);
414 }
415 }
416
417 return out;
418}
419
420
421let renderQueue = {
422 items: [],
423 itemsOffline: [],
424 pending: false,
425 add(component) {
426 if (renderQueue.items.push(component)!==1) return;
427
428 let d = hooks.debounceRendering;
429 if (d) d(renderQueue.process);
430 else setTimeout(renderQueue.process, 0);
431 },
432 process() {
433 let items = renderQueue.items,
434 len = items.length;
435 if (!len) return;
436 renderQueue.items = renderQueue.itemsOffline;
437 renderQueue.items.length = 0;
438 renderQueue.itemsOffline = items;
439 while (len--) {
440 if (items[len]._dirty) {
441 items[len]._render();
442 }
443 }
444 }
445};
446
447let rerender = renderQueue.process;
448export { rerender };
449
450
451/** Typed DOM node factory with reclaimation */
452let recycler = {
453 nodes: {},
454 collect(node) {
455 let name = node.nodeName;
456 recycler.clean(node);
457 let list = recycler.nodes[name] || (recycler.nodes[name] = []);
458 list.push(node);
459 },
460 create(nodeName) {
461 let list = recycler.nodes[name];
462 if (list && list.length) {
463 return list.splice(0, 1)[0];
464 }
465 return document.createElement(nodeName);
466 },
467 clean(node) {
468 node.remove();
469
470 if (node.attributes) {
471 let attrs = getNodeAttributes(node);
472 for (let attr in attrs) if (attrs.hasOwnProperty(attr)) {
473 node.removeAttribute(attr);
474 }
475 }
476
477 // if (node.childNodes.length>0) {
478 // console.warn(`Warning: Recycler collecting <${node.nodeName}> with ${node.childNodes.length} children.`);
479 // slice.call(node.childNodes).forEach(recycler.collect);
480 // }
481 }
482};
483
484
485let componentRecycler = {
486 components: {},
487 collect(component) {
488 let name = component.constructor.name;
489 let list = componentRecycler.components[name] || (componentRecycler.components[name] = []);
490 list.push(component);
491 },
492 create(ctor) {
493 let name = ctor.name,
494 list = componentRecycler.components[name];
495 if (list && list.length) {
496 return list.splice(0, 1)[0];
497 }
498 return new ctor();
499 }
500};
501
502
503function appendChildren(parent, children) {
504 let len = children.length;
505 if (len<=2) {
506 parent.appendChild(children[0]);
507 if (len===2) parent.appendChild(children[1]);
508 return;
509 }
510
511 let frag = document.createDocumentFragment();
512 for (let i=0; i<len; i++) frag.appendChild(children[i]);
513 parent.appendChild(frag);
514}
515
516
517function getAccessor(node, name, value) {
518 if (name==='class') return node.className;
519 if (name==='style') return node.style.cssText;
520 return value;
521 //return getComplexAccessor(node, name, value);
522}
523
524// function getComplexAccessor(node, name, value) {
525// let uc = 'g'+nameToAccessor(name).substring(1);
526// if (node[uc] && typeof node[uc]==='function') {
527// return node[uc]();
528// }
529// return value;
530// }
531
532
533/** Attempt to set via an accessor method, falling back to setAttribute().
534 * Automatically detects and adds/removes event handlers based for "attributes" beginning with "on".
535 * If `value=null`, triggers attribute/handler removal.
536 */
537function setAccessor(node, name, value, old) {
538 if (name==='class') {
539 node.className = value;
540 }
541 else if (name==='style') {
542 node.style.cssText = value;
543 }
544 else {
545 setComplexAccessor(node, name, value, old);
546 }
547}
548
549function eventProxy(e) {
550 let l = this._listeners,
551 fn = l[e.type.toLowerCase()];
552 if (fn) return fn.call(l, hook(hooks, 'event', e) || e);
553}
554
555function setComplexAccessor(node, name, value, old) {
556 if (name.substring(0,2)==='on') {
557 let type = name.substring(2).toLowerCase(),
558 l = node._listeners || (node._listeners = {});
559 if (!l[type]) node.addEventListener(type, eventProxy);
560 l[type] = value;
561 return;
562 }
563
564 let uc = nameToAccessor(name);
565 if (node[uc] && typeof node[uc]==='function') {
566 node[uc](value);
567 }
568 else if (value!==null) {
569 node.setAttribute(name, value);
570 }
571 else {
572 node.removeAttribute(name);
573 }
574}
575
576function nameToAccessor(name) {
577 let c = nameToAccessorCache[name];
578 if (!c) {
579 c = 'set' + name.charAt(0).toUpperCase() + name.substring(1);
580 nameToAccessorCache[name] = c;
581 }
582 return c;
583}
584let nameToAccessorCache = {};
585
586
587function getNodeAttributes(node) {
588 let list = node.attributes;
589 if (!list.getNamedItem) return list;
590 if (list.length) return getAttributesAsObject(list);
591}
592
593function getAttributesAsObject(list) {
594 let attrs = {};
595 for (let i=list.length; i--; ) {
596 let item = list[i];
597 attrs[item.name] = item.value;
598 }
599 return attrs;
600}
601
602
603function getNodeProps(vnode) {
604 let props = extend({}, vnode.attributes);
605 if (vnode.children) {
606 props.children = vnode.children;
607 }
608 if (vnode.text) {
609 props._content = vnode.text;
610 }
611 return props;
612}
613
614
615function extend(obj, props) {
616 for (let i in props) if (props.hasOwnProperty(i)) {
617 obj[i] = props[i];
618 }
619 return obj;
620}