UNPKG

awv3

Version:
275 lines (245 loc) 10.3 kB
import omit from 'lodash/omit' import flatten from 'lodash/flatten' import isEqual from 'lodash/isEqual' import Element from './element' import shallowEqual from 'shallow-equal/objects' import hash from 'string-hash' let queue = Promise.resolve(), currentId, frames = {} export class Component { constructor(props) { this.id = currentId this.plugin = frames[this.id].plugin this.node = undefined this.props = props || {} this.refs = {} //console.log(" constructor", this.constructor.name, this.plugin.type, this.plugin.id) } dispatch(action) { this.plugin.store.dispatch(action) } setState(state) { const frame = frames[this.id] const newState = { ...this.state, ...state } 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.reusable) frame.reusable = frame.reusable.sort((a, b) => a.order - b.order) this.node.children = prepare(this.render(this.props, this.state, this.setState.bind(this)), this.node.depth) frame.reusable = [] if (this.node.children.isArray) { let parent = this.node.children while (parent && parent.isArray && parent.parent) parent = parent.parent renderNodes(this.plugin, parent, parent.parent, true) } else { renderNodes(this.plugin, this.node.children, this.node.parent, true) } } } } export function connect(selector) { return DecoratedComponent => class ConnectedComponent 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 render(plugin, cb) { queue = queue.then(() => { currentId = plugin.id //console.log("render plugin", plugin.type, plugin.id) frames[currentId] && destroy(currentId) frames[currentId] = { stack: {}, reusable: [], order: 0, plugin: plugin, } plugin.addElement(renderNodes(plugin, prepare(cb()))) //console.log("finished rendering plugin", plugin.type, plugin.id) }) } export function destroy(id) { let frame = frames[id] if (frame) { Object.keys(frame.stack).forEach(key => destroyNode(frame, frame.stack[key])) frame.plugin.destroyElements() } } function destroyNode(frame, node, traverse = true) { if (Array.isArray(node)) return node.forEach(node => destroyNode(frame, node)) else if (node) { node.unsubscribes && node.unsubscribes.forEach(unsub => unsub()) if (node.component) { node.component.componentWillUnmount && node.component.componentWillUnmount() node.component = undefined } if (node.element) { const elements = Array.isArray(node.element) ? node.element : [node.element] elements.forEach(element => { let parent = node.parent while (parent && !parent.element) parent = parent.parent if (parent && parent.element) parent.element.children = parent.element.children.filter(id => id != element.id) element.destroy() }) node.element = undefined } traverse && destroyNode(frame, node.children, traverse) delete frame.stack[node.tag] } } export function buildStack(tree, target) { if (Array.isArray(tree)) return tree.forEach(node => buildStack(node, 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 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] tree.tag = tree.hash + '.' + tree.depth.join('.') 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)) } if (node && node.el) { const found = frame.stack[node.tag] const target = found ? found : node const element = target.element if (found && Array.isArray(node.children)) { // Remove missing array items or items without key const nodeArrays = node.children.filter(Array.isArray) target.children.filter(Array.isArray).forEach((targetArray, index) => { const nodeArray = nodeArrays[index] targetArray.forEach((targetChild, index) => { let test = targetChild.props.key !== undefined && nodeArray.find(nodeChild => targetChild.props.key === nodeChild.props.key) if (test === undefined) { destroyNode(frame, frame.stack[targetChild.tag]) delete targetArray[index] } }) }) } let flattenedChildren = Array.isArray(node.children) ? flatten(node.children) : node.children parent = parent || target.parent let parentElem = (parent || {}).element if (!found) { frame.stack[node.tag] = target } target.parent = parent if (target.el.prototype instanceof Element) { // Render children into Elements and remove missing items // Items become undefined when they are toggled by ternary const children = flatten(renderNodes(plugin, flattenedChildren, target)) for (let i = 0; i < children.length; i++) { children[i] === undefined && target.children[i] && frame.stack[target.children[i].tag] && destroyNode(frame, frame.stack[target.children[i].tag]) } const update = { ...node.props, children: children.filter(e => e).map(e => e.id), } if (found) { let newProps = Object.keys(update) .filter(key => !isEqual(update[key], target.element[key])) .reduce((acc, key) => ({ ...acc, [key]: update[key] }), {}) Object.keys(newProps).length > 0 && target.element.update(newProps) } else { target.element = new target.el(frame.plugin, update) } target.children = node.children 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 { return renderNodes(plugin, flattenedChildren, parent) } return target.element } } const React = { createElement(el, props, ...children) { const frame = frames[currentId] 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, hash: el.name, props: omit(props, handlers), handlers, events, children: children, } } else { let trace = frame.reusable.shift() let node = { el, hash: hash(el.toString()), props: props || {}, order: frame.order++ } if ( trace && trace.el === node.el && trace.hash === node.hash && trace.component.props.key === node.props.key ) { node = trace const newProps = { ...node.component.props, ...props, children } node.component.componentWillReceiveProps && node.component.componentWillReceiveProps(newProps) node.component.props = newProps } else { node.component = new el({ ...props, children }) 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), ) node.isArray = Array.isArray(node.children) return node } }, } export default React