mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] typing: make app and mount method properly generic
This commit is contained in:
committed by
Samuel Degueldre
parent
4b88787a72
commit
0dbc807e01
+22
-18
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user