import { computable, ComputableSelector, InferSelectorValue } from "../data/computable"; import { Component, ComponentConstructor, ComponentConfigType } from "../util/Component"; import { isArray } from "../util/isArray"; import { isFunction } from "../util/isFunction"; import { StoreProxy } from "../data/StoreProxy"; import { RenderingContext } from "./RenderingContext"; import { View, ViewMethods } from "../data/View"; import { Widget } from "./Widget"; import { Instance } from "./Instance"; import { AccessorChain } from "../data/createAccessorModelProxy"; const computablePrefix = "computable-"; const triggerPrefix = "trigger-"; interface ComputableEntry { name: string; selector: any; type: "computable" | "trigger"; } // Controller methods interface for ThisType in config objects export interface ControllerMethods { store: View; instance: Instance; widget?: Widget; invokeParentMethod(method: string, ...args: any[]): any; getParentControllerByType(type: new (...args: any[]) => T): T; addTrigger( name: string, args: [...Selectors], callback: (...values: { [K in keyof Selectors]: InferSelectorValue }) => any, autoRun?: boolean, ): void; addComputable( name: AccessorChain, args: [...Selectors], callback: (...values: { [K in keyof Selectors]: InferSelectorValue }) => T, ): void; addComputable( name: string, args: [...Selectors], callback: (...values: { [K in keyof Selectors]: InferSelectorValue }) => any, ): void; } export interface BaseControllerConfig { store?: View; instance?: Instance; widget?: Widget; onInit?: (context: RenderingContext) => void; onExplore?: (context: RenderingContext) => void; onPrepare?: (context: RenderingContext) => void; onCleanup?: (context: RenderingContext) => void; onDestroy?: () => void; } // Controller config object type with Controller methods AND its own methods available on 'this' export type ControllerConfig = T & ThisType; /** Controller factory function type */ export type ControllerFactory = ( config: ViewMethods, ) => ControllerConfig; export class Controller extends Component implements ControllerMethods { initialized?: boolean; onInit?(context: RenderingContext): void; onExplore?(context: RenderingContext): void; onPrepare?(context: RenderingContext): void; onCleanup?(context: RenderingContext): void; onDestroy?(): void; declare instance: Instance; declare store: View; declare widget?: Widget; computables?: Record; constructor(config?: BaseControllerConfig) { super(config); } init(context?: RenderingContext): void { if (!this.initialized) { this.initialized = true; if (this.onInit && context) this.onInit(context); } } explore(context: RenderingContext): void { let { store } = this.instance; this.store = store; //in rare cases instance may change its store if (!this.initialized) { this.init(context); //forgive if the developer forgets to call super.init() this.initialized = true; } if (this.computables) { for (let key in this.computables) { let x = this.computables[key]; let v = x.selector(store.getData()); if (x.type == "computable") store.set(x.name, v); } } if (this.onExplore) { this.onExplore(context); } } prepare(context: RenderingContext): void { if (this.onPrepare) { this.onPrepare(context); } } cleanup(context: RenderingContext): void { if (this.onCleanup) { this.onCleanup(context); } } addComputable( name: AccessorChain, args: [...Selectors], callback: (...values: { [K in keyof Selectors]: InferSelectorValue }) => T, ): void; addComputable( name: string, args: [...Selectors], callback: (...values: { [K in keyof Selectors]: InferSelectorValue }) => any, ): void; addComputable(name: string | AccessorChain, args: ComputableSelector[], callback: (...values: any[]) => any): void { if (!isArray(args)) throw new Error("Second argument to the addComputable method should be an array."); let selector = computable(...args, callback).memoize(); if (!this.computables) this.computables = {}; let nameStr = String(name); this.computables[computablePrefix + nameStr] = { name: nameStr, selector, type: "computable" }; } addTrigger( name: string, args: [...Selectors], callback: (...values: { [K in keyof Selectors]: InferSelectorValue }) => any, autoRun?: boolean, ): void { if (!isArray(args)) throw new Error("Second argument to the addTrigger method should be an array."); let selector = computable(...args, callback).memoize(!autoRun && this.store.getData()); if (!this.computables) this.computables = {}; this.computables[triggerPrefix + name] = { name, selector, type: "trigger" }; } removeTrigger(name: string): void { if (this.computables) delete this.computables[triggerPrefix + name]; } removeComputable(name: string): void { if (this.computables) delete this.computables[computablePrefix + name]; } invokeParentMethod(methodName: string, ...args: any[]): any { let parent = this.instance.parent; if (!parent) throw new Error("Cannot invoke a parent controller method as the instance does not have a parent."); return parent.invokeControllerMethod(methodName, ...args); } invokeMethod(methodName: string, ...args: any[]): any { return this.instance.invokeControllerMethod(methodName, ...args); } getParentControllerByType(type: new (...args: any[]) => T): T { return this.instance.getControllerByType(type); } } Controller.namespace = "ui.controller."; Controller.factory = function (alias: any, config?: any, more?: any) { if (isFunction(alias)) { let cfg = { ...config, ...more, }; if (cfg.instance) { //in rare cases instance.store may change, so we cannot rely on the store passed through configuration cfg.store = new StoreProxy(() => cfg.instance.store); Object.assign(cfg, cfg.store.getMethods()); } let result = alias(cfg); if (result instanceof Controller) return result; return Controller.create({ ...config, ...more, ...result, }); } return Controller.create({ ...config, ...more, }); };