|
| 1 | +import { EventType, IConfiguration, Overmind } from 'overmind' |
| 2 | +import { VALUE } from 'proxy-state-tree' |
| 3 | +import * as snabbdom from 'snabbdom' |
| 4 | +import h from 'snabbdom/h' |
| 5 | +import attributesModule from 'snabbdom/modules/attributes' |
| 6 | +import classModule from 'snabbdom/modules/class' |
| 7 | +import eventlistenersModule from 'snabbdom/modules/eventlisteners' |
| 8 | +import propsModule from 'snabbdom/modules/props' |
| 9 | +import styleModule from 'snabbdom/modules/style' |
| 10 | +import thunk from 'snabbdom/thunk' |
| 11 | + |
| 12 | +import { SELF, getName, normalizeAttrs } from './utils' |
| 13 | + |
| 14 | +const patch = snabbdom.init([ |
| 15 | + classModule, |
| 16 | + propsModule, |
| 17 | + attributesModule, |
| 18 | + styleModule, |
| 19 | + eventlistenersModule, |
| 20 | +]) |
| 21 | + |
| 22 | +let _app |
| 23 | + |
| 24 | +export interface IComponent<Config extends IConfiguration, Props = {}> { |
| 25 | + ( |
| 26 | + props: Props, |
| 27 | + overmind: { |
| 28 | + state: Overmind<Config>['state'] |
| 29 | + actions: Overmind<Config>['actions'] |
| 30 | + effects: Overmind<Config>['effects'] |
| 31 | + } |
| 32 | + ): any |
| 33 | +} |
| 34 | + |
| 35 | +declare global { |
| 36 | + namespace JSX { |
| 37 | + interface IntrinsicElements { |
| 38 | + self: React.DetailedHTMLProps< |
| 39 | + React.HtmlHTMLAttributes<HTMLHtmlElement> & { |
| 40 | + onMount?: (el: HTMLElement) => void | (() => void) |
| 41 | + }, |
| 42 | + HTMLHtmlElement |
| 43 | + > |
| 44 | + } |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +declare module 'react' { |
| 49 | + interface HTMLAttributes<T> |
| 50 | + extends React.AriaAttributes, |
| 51 | + React.DOMAttributes<T> { |
| 52 | + onInsert?: (el: HTMLElement) => void |
| 53 | + onUpdate?: (el: HTMLElement) => void |
| 54 | + onDestroy?: (el: HTMLElement) => void |
| 55 | + onEnter?: (el: HTMLElement) => void |
| 56 | + onLeave?: (el: HTMLElement) => void |
| 57 | + class?: { |
| 58 | + [key: string]: boolean |
| 59 | + } |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +const thunks = new Map() |
| 64 | + |
| 65 | +let nextId = 0 |
| 66 | +let nextComponentId = {} |
| 67 | +let nextComponentInstanceId = {} |
| 68 | + |
| 69 | +export const createElement = (tag, attrs, ...children) => { |
| 70 | + if (typeof tag === 'function') { |
| 71 | + const memo = Object.keys(attrs || {}).reduce( |
| 72 | + (aggr, key) => { |
| 73 | + const value = attrs[key] |
| 74 | + if (value && value[VALUE] && !value[VALUE].__rescope) { |
| 75 | + Object.defineProperty(value[VALUE], '__rescope', { |
| 76 | + get: () => { |
| 77 | + return (tree) => _app.proxyStateTree.rescope(value, tree) |
| 78 | + }, |
| 79 | + }) |
| 80 | + } |
| 81 | + return [...aggr, key, value && value[VALUE] ? value[VALUE] : value] |
| 82 | + }, |
| 83 | + children.length |
| 84 | + ? ['children', children.reduce((aggr, item) => aggr.concat(item), [])] |
| 85 | + : [] |
| 86 | + ) |
| 87 | + |
| 88 | + const name = getName(tag.name) |
| 89 | + return thunk( |
| 90 | + name, |
| 91 | + attrs && attrs.key, |
| 92 | + thunks.get(tag) || |
| 93 | + thunks |
| 94 | + .set(tag, (...props) => { |
| 95 | + const tree = _app.proxyStateTree.getTrackStateTree() |
| 96 | + const propsObject = {} |
| 97 | + while (props.length) { |
| 98 | + const [key, value] = props.splice(0, 2) |
| 99 | + propsObject[key] = |
| 100 | + value && value.__rescope ? value.__rescope(tree) : value |
| 101 | + } |
| 102 | + let vnode |
| 103 | + let elm |
| 104 | + |
| 105 | + function onFlush(_, __, flushId) { |
| 106 | + if (elm.__overmind.tree !== tree) { |
| 107 | + tree.dispose() |
| 108 | + return |
| 109 | + } |
| 110 | + |
| 111 | + tree.track(update) |
| 112 | + |
| 113 | + const newNode = tag(propsObject, { |
| 114 | + state: tree.state, |
| 115 | + actions: _app.actions, |
| 116 | + }) |
| 117 | + tree.stopTracking() |
| 118 | + |
| 119 | + patch(vnode, newNode) |
| 120 | + vnode = newNode |
| 121 | + |
| 122 | + _app.eventHub.emitAsync(EventType.COMPONENT_UPDATE, { |
| 123 | + componentId: elm.__overmind.componentId, |
| 124 | + componentInstanceId: elm.__overmind.componentInstanceId, |
| 125 | + name, |
| 126 | + flushId, |
| 127 | + paths: Array.from(tree.pathDependencies) as any, |
| 128 | + }) |
| 129 | + } |
| 130 | + |
| 131 | + tree.track(onFlush) |
| 132 | + |
| 133 | + vnode = tag(propsObject, { |
| 134 | + state: tree.state, |
| 135 | + actions: _app.actions, |
| 136 | + }) |
| 137 | + |
| 138 | + if (vnode.sel !== SELF) { |
| 139 | + throw new Error( |
| 140 | + `You are not returning a <self /> element from the component ${name} ` |
| 141 | + ) |
| 142 | + } |
| 143 | + |
| 144 | + vnode.data.hook = vnode.data.hook || {} |
| 145 | + const insert = vnode.data.hook.insert |
| 146 | + const update = vnode.data.hook.update |
| 147 | + const destroy = vnode.data.hook.destroy |
| 148 | + |
| 149 | + vnode.data.hook = { |
| 150 | + insert: (actualVnode) => { |
| 151 | + if (!(name in nextComponentId)) { |
| 152 | + nextComponentId[name] = nextId++ |
| 153 | + } |
| 154 | + if (!(name in nextComponentInstanceId)) { |
| 155 | + nextComponentInstanceId[name] = 0 |
| 156 | + } |
| 157 | + |
| 158 | + nextComponentInstanceId[name] = nextId++ |
| 159 | + |
| 160 | + elm = actualVnode.elm |
| 161 | + vnode.elm = elm |
| 162 | + elm.__overmind = Object.assign(elm.__overmind || {}, { |
| 163 | + name, |
| 164 | + tree, |
| 165 | + componentId: nextComponentId, |
| 166 | + componentInstanceId: nextComponentInstanceId[name], |
| 167 | + }) |
| 168 | + |
| 169 | + insert && insert(elm) |
| 170 | + |
| 171 | + _app.eventHub.emitAsync(EventType.COMPONENT_ADD, { |
| 172 | + componentId: elm.__overmind.componentId, |
| 173 | + componentInstanceId: elm.__overmind.componentInstanceId, |
| 174 | + name, |
| 175 | + paths: Array.from(tree.pathDependencies) as any, |
| 176 | + }) |
| 177 | + }, |
| 178 | + update: (_, vnode) => { |
| 179 | + elm = vnode.elm |
| 180 | + elm.__overmind.tree = tree |
| 181 | + update && update(vnode.elm) |
| 182 | + }, |
| 183 | + destroy: (vnode) => { |
| 184 | + elm = vnode.elm |
| 185 | + tree.dispose() |
| 186 | + destroy && destroy(vnode.elm) |
| 187 | + _app.eventHub.emitAsync(EventType.COMPONENT_REMOVE, { |
| 188 | + componentId: elm.__overmind.componentId, |
| 189 | + componentInstanceId: elm.__overmind.componentInstanceId, |
| 190 | + name, |
| 191 | + }) |
| 192 | + }, |
| 193 | + } |
| 194 | + |
| 195 | + tree.stopTracking() |
| 196 | + |
| 197 | + return vnode |
| 198 | + }) |
| 199 | + .get(tag), |
| 200 | + memo |
| 201 | + ) |
| 202 | + } |
| 203 | + |
| 204 | + return h( |
| 205 | + tag, |
| 206 | + normalizeAttrs(tag, attrs), |
| 207 | + children.reduce((aggr, item) => aggr.concat(item), []) |
| 208 | + ) |
| 209 | +} |
| 210 | + |
| 211 | +export const render = (app, vnode, container) => { |
| 212 | + _app = app |
| 213 | + patch(container, vnode()) |
| 214 | +} |
0 commit comments