[IMP] typing: make app and mount method properly generic

This commit is contained in:
Géry Debongnie
2022-01-27 14:28:01 +01:00
committed by Samuel Degueldre
parent 4b88787a72
commit 0dbc807e01
4 changed files with 55 additions and 36 deletions
+22 -18
View File
@@ -1,4 +1,4 @@
import { Component } from "../component/component"; import { Component, ComponentConstructor } from "../component/component";
import { ComponentNode } from "../component/component_node"; import { ComponentNode } from "../component/component_node";
import { MountOptions } from "../component/fibers"; import { MountOptions } from "../component/fibers";
import { Scheduler } from "../component/scheduler"; import { Scheduler } from "../component/scheduler";
@@ -11,9 +11,9 @@ export interface Env {
[key: string]: any; [key: string]: any;
} }
export interface AppConfig extends TemplateSetConfig { export interface AppConfig<P, E> extends TemplateSetConfig {
env?: Env; props?: P;
props?: any; env?: E;
} }
export const DEV_MSG = `Owl is running in 'dev' mode. 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. This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`; See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
export class App<T extends typeof Component = any> extends TemplateSet { export class App<
Root: T; T extends abstract new (...args: any) => any = any,
props: any; P = any,
env: Env; E = any
> extends TemplateSet {
Root: ComponentConstructor<P, E>;
props: P;
env: E;
scheduler = new Scheduler(); scheduler = new Scheduler();
root: ComponentNode | null = null; root: ComponentNode<P, E> | null = null;
constructor(Root: T, config: AppConfig = {}) { constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
super(config); super(config);
this.Root = Root; this.Root = Root;
if (config.dev) { if (config.dev) {
console.info(DEV_MSG); console.info(DEV_MSG);
} }
const descrs = Object.getOwnPropertyDescriptors(config.env || {}); const descrs = Object.getOwnPropertyDescriptors(config.env || {});
this.env = Object.freeze(Object.defineProperties({}, descrs)); this.env = Object.freeze(Object.defineProperties({}, descrs)) as E;
this.props = config.props || {}; this.props = config.props || ({} as P);
} }
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> { mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
this.checkTarget(target); this.checkTarget(target);
const node = this.makeNode(this.Root, this.props); const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options); const prom = this.mountNode(node, target, options);
@@ -56,7 +60,7 @@ export class App<T extends typeof Component = any> extends TemplateSet {
} }
} }
makeNode(Component: T, props: any): ComponentNode { makeNode(Component: ComponentConstructor, props: any): ComponentNode {
return new ComponentNode(Component, props, this); return new ComponentNode(Component, props, this);
} }
@@ -96,10 +100,10 @@ export class App<T extends typeof Component = any> extends TemplateSet {
} }
} }
export async function mount<T extends typeof Component>( export async function mount<T extends abstract new (...args: any) => any = any, P = any, E = any>(
C: T, C: T & ComponentConstructor<P, E>,
target: HTMLElement, target: HTMLElement,
config: AppConfig & MountOptions = {} config: AppConfig<P, E> & MountOptions = {}
): Promise<InstanceType<T>> { ): Promise<Component<P, E> & InstanceType<T>> {
return new App(C, config).mount(target, config); return new App(C, config).mount(target, config);
} }
+16
View File
@@ -4,9 +4,25 @@ import type { ComponentNode } from "./component_node";
// Component Class // Component Class
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type Props = { [key: string]: any };
interface StaticComponentProperties {
template: string;
defaultProps?: any;
props?: any;
}
export type ComponentConstructor<P extends Props = any, E = any> = (new (
props: P,
env: E,
node: ComponentNode
) => Component<P, E>) &
StaticComponentProperties;
export class Component<Props = any, Env = any> { export class Component<Props = any, Env = any> {
static template: string = ""; static template: string = "";
static props?: any; static props?: any;
static defaultProps?: any;
props: Props; props: Props;
env: Env; env: Env;
+4 -6
View File
@@ -1,6 +1,6 @@
import type { App, Env } from "../app/app"; import type { App, Env } from "../app/app";
import { BDom, VNode } from "../blockdom"; import { BDom, VNode } from "../blockdom";
import { Component } from "./component"; import { Component, ComponentConstructor } from "./component";
import { import {
Fiber, Fiber,
makeChildFiber, makeChildFiber,
@@ -74,13 +74,11 @@ export function component(
type LifecycleHook = Function; type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = typeof Component> export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E>> {
implements VNode<ComponentNode>
{
el?: HTMLElement | Text | undefined; el?: HTMLElement | Text | undefined;
app: App; app: App;
fiber: Fiber | null = null; fiber: Fiber | null = null;
component: InstanceType<T>; component: Component<P, E>;
bdom: BDom | null = null; bdom: BDom | null = null;
status: STATUS = STATUS.NEW; status: STATUS = STATUS.NEW;
@@ -99,7 +97,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
patched: LifecycleHook[] = []; patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = []; willDestroy: LifecycleHook[] = [];
constructor(C: T, props: any, app: App, parent?: ComponentNode) { constructor(C: ComponentConstructor<P, E>, props: P, app: App, parent?: ComponentNode) {
currentNode = this; currentNode = this;
this.app = app; this.app = app;
this.parent = parent || null; this.parent = parent || null;
+13 -12
View File
@@ -1,16 +1,16 @@
import { Component } from "./component"; import { ComponentConstructor } from "./component";
/** /**
* Apply default props (only top level). * Apply default props (only top level).
* *
* Note that this method does modify in place the props * Note that this method does modify in place the props
*/ */
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) { export function applyDefaultProps<P>(props: P, ComponentClass: ComponentConstructor<P>) {
const defaultProps = (ComponentClass as any).defaultProps; const defaultProps = ComponentClass.defaultProps;
if (defaultProps) { if (defaultProps) {
for (let propName in defaultProps) { for (let propName in defaultProps) {
if (props![propName] === undefined) { if ((props as any)[propName] === undefined) {
props![propName] = defaultProps[propName]; (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. * visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode. * This is why it is only done in 'dev' mode.
*/ */
export const validateProps = function (name: string | typeof Component, props: any, parent?: any) { export function validateProps<P>(name: string | ComponentConstructor<P>, props: P, parent?: any) {
const ComponentClass = ( const ComponentClass =
typeof name !== "string" ? name : parent.constructor.components[name] typeof name !== "string"
) as typeof Component; ? name
: (parent.constructor.components[name] as ComponentConstructor<P> | undefined);
if (!ComponentClass) { if (!ComponentClass) {
// this is an error, wrong component. We silently return here instead so the // 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 === "*") { if (propName === "*") {
continue; continue;
} }
if (props[propName] === undefined) { if ((props as any)[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) { if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`); throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
} else { } else {
@@ -62,7 +63,7 @@ export const validateProps = function (name: string | typeof Component, props: a
} }
let isValid; let isValid;
try { try {
isValid = isValidProp(props[propName], propsDef[propName]); isValid = isValidProp((props as any)[propName], propsDef[propName]);
} catch (e) { } catch (e) {
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${ (e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message (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 * Check if an invidual prop value matches its (static) prop definition