UNPKG

awv3

Version:
211 lines (184 loc) 7.26 kB
import omit from 'lodash/omit'; import flatten from 'lodash/flatten'; import Element from './element'; import shallowEqual from 'shallow-equal/objects'; import hash from 'string-hash'; let queue = Promise.resolve(), currentId, frames = window.f = { }; export function render(plugin, cb) { requestAnimationFrame(() => { queue = queue.then(() => { currentId = plugin.id; frames[currentId] && destroy(currentId); frames[currentId] = { stack: {}, stateStack: [], order: 0, plugin: plugin } plugin.addElement(renderNodes(plugin, prepare(cb()))); }); }); } export function destroy(id) { let frame = frames[id]; if (frame) { Object.values(frame.stack).forEach(node => { node.unsubscribes && node.unsubscribes.forEach(unsub => unsub()); node.component && node.component.componentWillUnmount && node.component.componentWillUnmount(); node.component = undefined; node.element.destroy(); node.element = undefined; node.children = undefined; }); delete frames[id]; frame.plugin.destroyElements(); } } export function connect(selector) { return DecoratedComponent => class extends Component { state = {}; componentWillMount() { let oldState = {}; let store = this.plugin.store; this.unsubscribe = store.subscribe(() => { let selectedState = selector(store.getState(), this.props); let changedKeys; Object.entries(selectedState).map(([key, value]) => { if (oldState[key] !== value) { changedKeys = { ...changedKeys, [key]: value }; oldState[key] = value; } }); changedKeys && this.setState(changedKeys); }); this.state = { ...this.state, ...selector(store.getState(), this.props) }; } componentWillUnmount() { this.unsubscribe(); } render() { const props = { ...this.props, ...this.state }; return <DecoratedComponent {...props} />; } }; } export function buildStack(tree, target) { (Array.isArray(tree) ? tree : [tree]).forEach(child => { child.el && !(child.el.prototype instanceof Element) && target.push(child); child.children && buildStack(child.children, target); }); } export class Component { constructor(props) { this.id = currentId; this.plugin = frames[this.id].plugin; this.node; this.props = props || {}; this.refs = {}; } dispatch(action) { this.plugin.store.dispatch(action); } setState(props) { const frame = frames[this.id]; const newState = { ...this.state, ...props }; if (!shallowEqual(newState, this.state)) { this.state = newState; // Build component stack & construct virtual dom currentId = this.id; frame.order = this.node.order; buildStack(this.node.children, frame.stateStack); frame.stateStack = frame.stateStack.sort((a, b) => a.order - b.order); this.node.children = prepare(this.render(this.node.props, this.state, this.setState.bind(this)), this.node.depth); frame.stateStack = []; renderNodes(this.plugin, this.node.children, this.node.parent, true); } } } export function prepare(tree, depth = [0]) { if (Array.isArray(tree)) { for (let i in tree) tree[i] = prepare(tree[i] || {}, [...depth, parseInt(i)]); } else { tree.depth = [...depth]; if (tree.children) tree.children = prepare(tree.children, depth); } return tree; } function renderNodes(plugin, node, parent, reconcile = false) { const frame = frames[plugin.id]; if (Array.isArray(node)) { return node.map(node => renderNodes(plugin, node, parent)); } else if (node.el) { let tag = node.tag + '.' + node.depth.join('.'); let found = frame.stack[tag]; let target = found ? found : node; let element = target.element; parent = parent || target.parent; let parentElem = (parent || {}).element; //console.log(tag, node.props, ', found: ', !!found, ', frame: ', currentId); if (!found) { frame.stack[tag] = target; } target.parent = parent; if (target.el.prototype instanceof Element) { const update = { ...node.props, children: renderNodes(plugin, node.children, target).filter(e => e).map(e => e.id) }; if (found) { target.element.update(update) } else { target.element = new target.el(frame.plugin, update); } target.unsubscribes && target.unsubscribes.forEach(unsub => unsub()); target.unsubscribes = target.handlers.map(handler => { const name = handler.charAt(2).toLowerCase() + handler.substr(3); return target.element.observe(state => state[name], (state, old) => node.events[handler](state, old)); }); } else { target.element = renderNodes(plugin, node.children, parent); } return target.element; } } const React = { createElement(el, props, ...children) { const frame = frames[currentId]; children = flatten(children); if (el.prototype instanceof Element) { const handlers = Object.keys(props || {}).filter( key => typeof props[key] === 'function' && key.startsWith('on'), ); const events = handlers.reduce((acc, val) => ({ ...acc, [val]: props[val] }), {}); return { el, tag: el.name, props: omit(props, handlers), handlers, events, children: children, }; } else { let state; let trace = frame.stateStack.shift(); let node = { el, tag: hash(el.toString()), props: props || {}, order: frame.order++ }; //if (trace) console.log(' diff', trace.tag, node.tag); if (trace && trace.el === node.el && trace.tag === node.tag) { state = trace.component.state; node.order = trace.order; } node.component = new el({ ...props, children }); node.component.state = { ...node.component.state, ...state }; node.component.node = node; node.component.componentWillMount && node.component.componentWillMount(); node.children = node.component.render( node.component.props, node.component.state, node.component.setState.bind(node.component), ); return node; } }, }; export default React;