UNPKG

@winged/core

Version:

Morden webapp framekwork made only for ts developers. (UNDER DEVELOPMENT, PLEASE DO NOT USE)

431 lines (393 loc) 15.5 kB
import 'reflect-metadata' import { Constructor, ModificationTree, NonFuncPropNames, Optional, RSDObject } from './types' import { RenderableStateDescriber } from './types' import { utils } from './utils' import { LNSlot } from './vdom/logicalNode/LNSlot' import { LNSlotPlugin } from './vdom/logicalNode/LNSlotPlugin' import { LNSubview } from './vdom/logicalNode/LNSubview' import { PrevSibling, vDomFactory, VDomRoot, VDomStruct, VNodeRegister } from './vdom/vdom' import { vdomUtils } from './vdom/vdomUtils' import { ViewModel } from './viewModel/ViewModel' type StateNames<V> = Exclude<NonFuncPropNames<V>, 'refs' | '_propsType' | '_events' | '_contents' | '_subviewMap'> type States<V> = Pick<V, StateNames<V>> export type VEventData< V extends View<any>, N extends keyof V['_subviewMap'], E extends keyof EV, EV = InstanceType<V['_subviewMap'][N]>['_events'], F = EV[E]> = F extends (data: infer A) => void ? A : void export type VSubviewType<V extends View<any>, P> = Constructor<V & { _propsType: P }> const M_SUBVIEW_PROPS = Symbol('custom:SubviewProps') const M_STATES = Symbol('custom:ViewStates') interface ViewStateMeta { className: string describerMerged: boolean renderableStateDescriber: RSDObject computedStates: { [stateName: string]: { getter: () => any; firstLevelDependencies: { [field: string]: true }; } } stateWatchers: Array<{ callback: () => any; firstLevelDependencies: { [field: string]: true }; }> } interface ViewClassReflection { defineState: (metadata: ViewStateMeta, stateName: string, describer?: RenderableStateDescriber) => void defineSubviewProps: (metadata: ViewStateMeta, stateName: string, describer?: RenderableStateDescriber) => void defineComputedState: (metadata: ViewStateMeta, getter: () => any, stateName: string, dependencies: string[]) => void defineStateWatcher: (metadata: ViewStateMeta, callback: () => any, dependencies: string[]) => void getMetadata(instance: BaseView): ViewStateMeta } type BaseView = View<any> interface SubviewPropsInfo { [name: string]: { dependencies: string[], getter: () => any } } export function vSubviewMap(...subviewPropDescribers: RenderableStateDescriber[]) { return (view: BaseView, _: string) => { const describers: RenderableStateDescriber = {} for (const d of subviewPropDescribers) { vdomUtils.mergeStateDescribers(describers, d) } Reflect.defineMetadata(M_SUBVIEW_PROPS, describers, view) } } export function vState(describer?: RenderableStateDescriber) { return (view: BaseView, stateName: string) => { const ViewReflect: ViewClassReflection = View as any const metadata = ViewReflect.getMetadata(view) ViewReflect.defineState(metadata, stateName, describer) } } export function vComputed<V extends BaseView>(...dependencies: Array<StateNames<V>>) { return <T>(view: BaseView, stateName: string, descriptor: TypedPropertyDescriptor<T>) => { const ViewReflect: ViewClassReflection = View as any const metadata = ViewReflect.getMetadata(view) if (!descriptor || !descriptor.get) { throw new Error( `Invalid computedState '${stateName}' in ${metadata.className}: Must be defined as a getter` ) } if (descriptor.set) { throw new Error( `Invalid computedState '${stateName}' in ${metadata.className}: Can't have a setter` ) } ViewReflect.defineComputedState(metadata, descriptor.get, stateName, dependencies as string[]) } } export function vWatch<V extends BaseView>(...dependencies: Array<StateNames<V>>) { return <T>(view: BaseView, key: string, descriptor: TypedPropertyDescriptor<T>) => { const ViewReflect: ViewClassReflection = View as any const metadata = ViewReflect.getMetadata(view) const callback = (view as any)[key] if (typeof callback !== 'function') { throw new Error(' Decorator vWatch must be used on a method member') } ViewReflect.defineStateWatcher(metadata, callback, dependencies as string[]) } } export abstract class View<V extends View<any>> { private get _viewClassName() { return `[${this.constructor.name}]` } public static readonly propsDescriber: RenderableStateDescriber = {} private static getMetadata(instance: BaseView): ViewStateMeta { let metadata: ViewStateMeta = Reflect.getMetadata(M_STATES, instance) if (!metadata) { metadata = { describerMerged: false, renderableStateDescriber: {}, computedStates: {}, stateWatchers: [], className: instance._viewClassName } Reflect.defineMetadata(M_STATES, metadata, instance) } return metadata } private static defineState(metadata: ViewStateMeta, stateName: string, describer?: RenderableStateDescriber) { const { renderableStateDescriber } = metadata if (renderableStateDescriber[stateName]) { return } renderableStateDescriber[stateName] = describer || true } private static defineComputedState( metadata: ViewStateMeta, definedGetter: () => any, stateName: string, dependencies: string[] ) { if (metadata.computedStates[stateName]) { return } const firstLevelDependencies = utils.listToMap(dependencies) // check loop structure for (const field in metadata.computedStates) { const info = metadata.computedStates[field] if ( info.firstLevelDependencies[stateName] && firstLevelDependencies[field] ) { throw new Error( `ComputedState "${field}" and "${stateName}" has bidirectional dependencies` ) } } metadata.computedStates[stateName] = { getter: definedGetter, firstLevelDependencies } } private static defineStateWatcher(metadata: ViewStateMeta, callback: () => any, dependencies: string[]) { metadata.stateWatchers.push({ callback, firstLevelDependencies: utils.listToMap(dependencies) }) } // ============ // core part // ============ public abstract _propsType: {} public abstract readonly _events: { [k: string]: (data?: any) => void } public abstract readonly _subviewMap: { [k: string]: Constructor<View<any>> } protected abstract readonly _contents: VDomStruct protected readonly refs: { [ref: string]: HTMLElement } private _metadata: ViewStateMeta = View.getMetadata(this) private _vNodeRegister: VNodeRegister private _vRoot?: VDomRoot private _viewModel: ViewModel private _subviewPropsInfo: SubviewPropsInfo private _initialProps: this['_propsType'] private _slotPlugins: { [slotName: string]: LNSlotPlugin } private _insertTarget?: { container: HTMLElement, prevSiblingNode?: Node } constructor(props?: V['_propsType']) { this._initialProps = props || {} const stateMeta = View.getMetadata(this) if (!stateMeta.describerMerged) { this.mergeStateMetaDescribers(stateMeta) } this._viewModel = new ViewModel({}, stateMeta.renderableStateDescriber) this._viewModel._watchBy(this, (modificationTree) => { this.update(modificationTree) }) this.initView() } // ============ // logical part // ============ public appendTo(container: HTMLElement) { if (this._insertTarget || this._vRoot && this._vRoot.domCreated) { throw new Error(`Can't call .appendTo() of ${this._viewClassName} because it's already rendered`) } if (this._vRoot) { this.initialRender(container, { node: undefined }) this.onMount(this._initialProps) delete this._initialProps } else { this._insertTarget = { container } } } public insertAfter(prevSiblingNode: Node) { if (this._insertTarget || this._vRoot && this._vRoot.domCreated) { throw new Error(`Can't call .insertAfter() of ${this._viewClassName} because it's already rendered`) } if (this._vRoot) { this.initialRender(prevSiblingNode.parentElement!, { node: prevSiblingNode }) this.onMount(this._initialProps) delete this._initialProps } else { this._insertTarget = { container: prevSiblingNode.parentElement!, prevSiblingNode } } } /** nothing but a shorthand, do the exact same thing as direct assignment */ // tslint:disable-next-line:no-empty public setState(state: Optional<States<V>>) { } // life cycle methods // tslint:disable-next-line:no-empty protected onMount(props: this['_propsType']) { } // tslint:disable-next-line:no-empty protected onPropsChange(props: this['_propsType']) { } // tslint:disable-next-line:no-empty protected onBeforeDestroy() { } /** abstract v.ts view class will generate and implement this */ protected getPropsDescriber(): RenderableStateDescriber | null { return null } protected abstract _linkSubviewProps(): SubviewPropsInfo private mergeStateMetaDescribers(stateMeta: ViewStateMeta) { const subviewPropsMeta = Reflect.getMetadata(M_SUBVIEW_PROPS, this) vdomUtils.mergeStateDescribers(stateMeta.renderableStateDescriber, subviewPropsMeta) for (const stateName in stateMeta.computedStates) { for (const name in stateMeta.computedStates[stateName].firstLevelDependencies) { if (!stateMeta.renderableStateDescriber[name]) { stateMeta.renderableStateDescriber[name] = true } } } for (const stateName in stateMeta.stateWatchers) { for (const name in stateMeta.stateWatchers[stateName].firstLevelDependencies) { if (!stateMeta.renderableStateDescriber[name]) { stateMeta.renderableStateDescriber[name] = true } } } stateMeta.describerMerged = true Reflect.defineMetadata(M_STATES, stateMeta, this) } private initView() { this.defineProperties() this._subviewPropsInfo = this._linkSubviewProps() // wait for impl class constructor to initialize property setTimeout(() => { this.createVRoot() }, 0) // NOTE: suppressing unused private method exception; // tslint:disable-next-line:no-unused-expression View.defineState // tslint:disable-next-line:no-unused-expression View.defineComputedState // tslint:disable-next-line:no-unused-expression View.defineStateWatcher // tslint:disable-next-line:no-unused-expression this.setPropsFromParent // tslint:disable-next-line:no-unused-expression this.setSlotPluginsFromParent // tslint:disable-next-line:no-unused-expression this.destroy } private destroy() { this.onBeforeDestroy() if (this._viewModel) { this._viewModel._destory() delete this._viewModel } if (this._vRoot) { this._vRoot.destroy() } this._vNodeRegister.destory() delete this._vRoot delete this._vNodeRegister delete this._metadata } private createVRoot() { // parse vdom expressions const register = new VNodeRegister(this) register.registerSubviewNode = this.registerSubviewNode.bind(this) register.registerSlotNode = this.registerSlotNode.bind(this) this._vNodeRegister = register this._vRoot = vDomFactory.createVDomRoot(this._contents, register) if (this._insertTarget) { const { container, prevSiblingNode } = this._insertTarget delete this._insertTarget if (prevSiblingNode) { this.insertAfter(prevSiblingNode) } else { this.appendTo(container) } } } private defineProperties() { const { renderableStateDescriber, computedStates } = this._metadata console.log('METADATA', this._metadata) // define states // define computed states for (const field in computedStates) { Object.defineProperty(this, field, { get: () => this._viewModel._get(field), set: () => { throw new Error(`Computed State '${field}' of ${this._viewClassName} is not writable`) }, configurable: false }) } for (const field in renderableStateDescriber) { if (computedStates[field]) { continue } Object.defineProperty(this, field, { get() { return (this as V)._viewModel._get(field) }, set(value) { (this as V)._viewModel._set(field, value) } }) } // define propsType Object.defineProperty(this, '_propsType', { get: () => { throw new Error('View.propsType is only for typing purpose, can\'t be get as a property') }, set: () => { throw new Error('View.propsType is only for typing purpose, can\'t be set as a property') } }) } private setPropsFromParent(props: this['_propsType']) { if (!this._vRoot) { this._initialProps = props } else { this.onPropsChange(props) } } private setSlotPluginsFromParent(slotPlugins: { [slotName: string]: LNSlotPlugin }) { this._slotPlugins = slotPlugins } // ============ // render part // ============ private registerSubviewNode(node: LNSubview): { propsGetter: () => any, stateDependencies: string[] } { if (!this._subviewPropsInfo[node.subviewName]) { console.error(this._subviewPropsInfo) throw new Error(`Can't find subview props info for ${node.subviewName} in ${this._viewClassName}`) } node.setViewClass(this._subviewMap[node.viewClassPath]) for (const eventName in node.outputEventNames) { const handlerName = node.outputEventNames[eventName] const handler = this[handlerName as keyof this] if (typeof handler !== 'function') { throw new Error(`invalid subview event handler. handlerName:${handlerName}`) } node.outputEventHandlers[eventName] = handler } const { dependencies, getter } = this._subviewPropsInfo[node.subviewName] return { propsGetter: getter, stateDependencies: dependencies } } private registerSlotNode(node: LNSlot) { node.registerSlotPlugin(this._slotPlugins[node.slotName]) } private initialRender(container: HTMLElement, prevSibling: PrevSibling) { // initial evaluation of computed states (this._vRoot as VDomRoot).initialRender(this._viewModel, container, prevSibling) } private update(modificationTree: ModificationTree) { const { computedStates, stateWatchers } = View.getMetadata(this) if (this._vRoot) { console.log('UPDATE', modificationTree) this._vRoot.update(this._viewModel, modificationTree) } // TODO: need to consider dead loops: // - like bidirectional computed state dependencies // - computedState "b" depends on "a", and a watcher change "a" on the change of "b" for (const entryField in modificationTree) { // update computed states for (const computedStateName in computedStates) { const { firstLevelDependencies, getter } = computedStates[computedStateName] if (firstLevelDependencies[entryField]) { let res: any = null try { res = getter.call(this) } catch (error) { console.warn(`Unable to calculate computedState '${computedStateName}' of ${this._viewClassName}:`) console.warn(error) } if (res !== this._viewModel._get(computedStateName)) { console.log('recalculate computed state', computedStateName, JSON.stringify(res)) this._viewModel._set(computedStateName, res) } } } // update watchers for (const watcherInfo of stateWatchers) { if (watcherInfo.firstLevelDependencies[entryField]) { watcherInfo.callback() } } } } }