From 0dbc807e0164875e1c48bf630c0dba70dcd2c2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 27 Jan 2022 14:28:01 +0100 Subject: [PATCH] [IMP] typing: make app and mount method properly generic --- src/app/app.ts | 40 +++++++++++++++++-------------- src/component/component.ts | 16 +++++++++++++ src/component/component_node.ts | 10 ++++---- src/component/props_validation.ts | 25 +++++++++---------- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/src/app/app.ts b/src/app/app.ts index a4e8f84d..ff8d9d13 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,4 +1,4 @@ -import { Component } from "../component/component"; +import { Component, ComponentConstructor } from "../component/component"; import { ComponentNode } from "../component/component_node"; import { MountOptions } from "../component/fibers"; import { Scheduler } from "../component/scheduler"; @@ -11,9 +11,9 @@ export interface Env { [key: string]: any; } -export interface AppConfig extends TemplateSetConfig { - env?: Env; - props?: any; +export interface AppConfig extends TemplateSetConfig { + props?: P; + env?: E; } export const DEV_MSG = `Owl is running in 'dev' mode. @@ -21,25 +21,29 @@ export const DEV_MSG = `Owl is running in 'dev' mode. This is not suitable for production use. See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`; -export class App extends TemplateSet { - Root: T; - props: any; - env: Env; +export class App< + T extends abstract new (...args: any) => any = any, + P = any, + E = any +> extends TemplateSet { + Root: ComponentConstructor; + props: P; + env: E; scheduler = new Scheduler(); - root: ComponentNode | null = null; + root: ComponentNode | null = null; - constructor(Root: T, config: AppConfig = {}) { + constructor(Root: ComponentConstructor, config: AppConfig = {}) { super(config); this.Root = Root; if (config.dev) { console.info(DEV_MSG); } const descrs = Object.getOwnPropertyDescriptors(config.env || {}); - this.env = Object.freeze(Object.defineProperties({}, descrs)); - this.props = config.props || {}; + this.env = Object.freeze(Object.defineProperties({}, descrs)) as E; + this.props = config.props || ({} as P); } - mount(target: HTMLElement, options?: MountOptions): Promise> { + mount(target: HTMLElement, options?: MountOptions): Promise & InstanceType> { this.checkTarget(target); const node = this.makeNode(this.Root, this.props); const prom = this.mountNode(node, target, options); @@ -56,7 +60,7 @@ export class App extends TemplateSet { } } - makeNode(Component: T, props: any): ComponentNode { + makeNode(Component: ComponentConstructor, props: any): ComponentNode { return new ComponentNode(Component, props, this); } @@ -96,10 +100,10 @@ export class App extends TemplateSet { } } -export async function mount( - C: T, +export async function mount any = any, P = any, E = any>( + C: T & ComponentConstructor, target: HTMLElement, - config: AppConfig & MountOptions = {} -): Promise> { + config: AppConfig & MountOptions = {} +): Promise & InstanceType> { return new App(C, config).mount(target, config); } diff --git a/src/component/component.ts b/src/component/component.ts index 36bfbe82..f1fbe001 100644 --- a/src/component/component.ts +++ b/src/component/component.ts @@ -4,9 +4,25 @@ import type { ComponentNode } from "./component_node"; // Component Class // ----------------------------------------------------------------------------- +type Props = { [key: string]: any }; + +interface StaticComponentProperties { + template: string; + defaultProps?: any; + props?: any; +} + +export type ComponentConstructor

= (new ( + props: P, + env: E, + node: ComponentNode +) => Component) & + StaticComponentProperties; + export class Component { static template: string = ""; static props?: any; + static defaultProps?: any; props: Props; env: Env; diff --git a/src/component/component_node.ts b/src/component/component_node.ts index b0c97864..6e1da229 100644 --- a/src/component/component_node.ts +++ b/src/component/component_node.ts @@ -1,6 +1,6 @@ import type { App, Env } from "../app/app"; import { BDom, VNode } from "../blockdom"; -import { Component } from "./component"; +import { Component, ComponentConstructor } from "./component"; import { Fiber, makeChildFiber, @@ -74,13 +74,11 @@ export function component( type LifecycleHook = Function; -export class ComponentNode - implements VNode -{ +export class ComponentNode

implements VNode> { el?: HTMLElement | Text | undefined; app: App; fiber: Fiber | null = null; - component: InstanceType; + component: Component; bdom: BDom | null = null; status: STATUS = STATUS.NEW; @@ -99,7 +97,7 @@ export class ComponentNode patched: LifecycleHook[] = []; willDestroy: LifecycleHook[] = []; - constructor(C: T, props: any, app: App, parent?: ComponentNode) { + constructor(C: ComponentConstructor, props: P, app: App, parent?: ComponentNode) { currentNode = this; this.app = app; this.parent = parent || null; diff --git a/src/component/props_validation.ts b/src/component/props_validation.ts index 015d0345..0e7b5594 100644 --- a/src/component/props_validation.ts +++ b/src/component/props_validation.ts @@ -1,16 +1,16 @@ -import { Component } from "./component"; +import { ComponentConstructor } from "./component"; /** * Apply default props (only top level). * * Note that this method does modify in place the props */ -export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) { - const defaultProps = (ComponentClass as any).defaultProps; +export function applyDefaultProps

(props: P, ComponentClass: ComponentConstructor

) { + const defaultProps = ComponentClass.defaultProps; if (defaultProps) { for (let propName in defaultProps) { - if (props![propName] === undefined) { - props![propName] = defaultProps[propName]; + if ((props as any)[propName] === undefined) { + (props as any)[propName] = defaultProps[propName]; } } } @@ -34,10 +34,11 @@ function getPropDescription(staticProps: any) { * visit recursively the props and all the children to check if they are valid. * This is why it is only done in 'dev' mode. */ -export const validateProps = function (name: string | typeof Component, props: any, parent?: any) { - const ComponentClass = ( - typeof name !== "string" ? name : parent.constructor.components[name] - ) as typeof Component; +export function validateProps

(name: string | ComponentConstructor

, props: P, parent?: any) { + const ComponentClass = + typeof name !== "string" + ? name + : (parent.constructor.components[name] as ComponentConstructor

| undefined); if (!ComponentClass) { // this is an error, wrong component. We silently return here instead so the @@ -53,7 +54,7 @@ export const validateProps = function (name: string | typeof Component, props: a if (propName === "*") { continue; } - if (props[propName] === undefined) { + if ((props as any)[propName] === undefined) { if (propsDef[propName] && !propsDef[propName].optional) { throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`); } else { @@ -62,7 +63,7 @@ export const validateProps = function (name: string | typeof Component, props: a } let isValid; try { - isValid = isValidProp(props[propName], propsDef[propName]); + isValid = isValidProp((props as any)[propName], propsDef[propName]); } catch (e) { (e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${ (e as Error).message @@ -80,7 +81,7 @@ export const validateProps = function (name: string | typeof Component, props: a } } } -}; +} /** * Check if an invidual prop value matches its (static) prop definition