Files
owl/src/runtime/app.ts
T

190 lines
5.6 KiB
TypeScript
Raw Normal View History

import { Component, ComponentConstructor, Props } from "./component";
2022-05-26 23:45:40 +02:00
import { ComponentNode } from "./component_node";
import { nodeErrorHandlers } from "./error_handling";
import { Fiber, MountOptions } from "./fibers";
2022-05-26 23:45:40 +02:00
import { Scheduler } from "./scheduler";
import { STATUS } from "./status";
import { validateProps } from "./template_helpers";
2021-12-20 17:32:32 +01:00
import { TemplateSet, TemplateSetConfig } from "./template_set";
2022-05-26 23:45:40 +02:00
import { validateTarget } from "./utils";
2020-11-26 16:45:25 +01:00
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
export interface Env {
[key: string]: any;
}
export interface AppConfig<P, E> extends TemplateSetConfig {
props?: P;
env?: E;
2022-01-28 13:39:03 +01:00
test?: boolean;
warnIfNoStaticProps?: boolean;
2021-10-20 16:08:31 +02:00
}
2022-03-03 08:07:05 +01:00
let hasBeenLogged = false;
export const DEV_MSG = () => {
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
return `Owl is running in 'dev' mode.
2021-10-20 16:08:31 +02:00
This is not suitable for production use.
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
};
2021-10-20 16:08:31 +02:00
export class App<
T extends abstract new (...args: any) => any = any,
P extends object = any,
E = any
> extends TemplateSet {
static validateTarget = validateTarget;
Root: ComponentConstructor<P, E>;
props: P;
env: E;
scheduler = new Scheduler();
root: ComponentNode<P, E> | null = null;
warnIfNoStaticProps: boolean;
2020-11-26 16:45:25 +01:00
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
2021-12-20 17:32:32 +01:00
super(config);
2020-11-26 16:45:25 +01:00
this.Root = Root;
2022-01-28 13:39:03 +01:00
if (config.test) {
this.dev = true;
}
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
2022-03-03 08:07:05 +01:00
if (this.dev && !config.test && !hasBeenLogged) {
console.info(DEV_MSG());
2022-03-03 08:07:05 +01:00
hasBeenLogged = true;
2021-10-20 16:08:31 +02:00
}
const env = config.env || {};
const descrs = Object.getOwnPropertyDescriptors(env);
this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
this.props = config.props || ({} as P);
2020-11-26 16:45:25 +01:00
}
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
App.validateTarget(target);
if (this.dev) {
2022-05-26 15:09:09 +02:00
validateProps(this.Root, this.props, { __owl__: { app: this } });
}
const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options);
this.root = node;
return prom;
}
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
return new ComponentNode(Component, props, this, null, null);
}
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
const promise: any = new Promise((resolve, reject) => {
2021-12-15 15:55:13 +01:00
let isResolved = false;
// manually set a onMounted callback.
// that way, we are independant from the current node.
node.mounted.push(() => {
resolve(node.component);
2021-12-15 15:55:13 +01:00
isResolved = true;
});
// Manually add the last resort error handler on the node
let handlers = nodeErrorHandlers.get(node);
if (!handlers) {
handlers = [];
nodeErrorHandlers.set(node, handlers);
}
handlers.unshift((e) => {
2021-12-15 15:55:13 +01:00
if (isResolved) {
console.error(e);
} else {
reject(e);
}
throw e;
});
});
node.mountComponent(target, options);
return promise;
2020-11-26 16:45:25 +01:00
}
destroy() {
if (this.root) {
this.scheduler.flush();
2020-11-26 16:45:25 +01:00
this.root.destroy();
}
}
createComponent<P extends Props>(
name: string | null,
isStatic: boolean,
hasSlotsProp: boolean,
hasDynamicPropList: boolean
) {
const isDynamic = !isStatic;
function _arePropsDifferent(props1: Props, props2: Props): boolean {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
}
}
return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length;
}
const arePropsDifferent = hasSlotsProp ? () => true : _arePropsDifferent;
return (props: P, key: string, ctx: ComponentNode, parent: any, C: any) => {
let children = ctx.children;
let node: any = children[key];
if (node && node.status === STATUS.DESTROYED) {
node = undefined;
}
if (isDynamic && node && node.component.constructor !== C) {
node = undefined;
}
const parentFiber = ctx.fiber!;
if (node) {
let shouldRender = node.forceNextRender;
if (shouldRender) {
node.forceNextRender = false;
} else {
const currentProps = node.props;
shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
}
if (shouldRender) {
node.updateAndRender(props, parentFiber);
}
} else {
// new component
if (isStatic) {
C = parent.constructor.components[name as any];
if (!C) {
throw new Error(`Cannot find the definition of component "${name}"`);
} else if (!(C.prototype instanceof Component)) {
throw new Error(
`"${name}" is not a Component. It must inherit from the Component class`
);
}
}
node = new ComponentNode(C, props, this, ctx, key);
children[key] = node;
node.initiateRender(new Fiber(node, parentFiber));
}
parentFiber.childrenMap[key] = node;
return node;
};
}
2020-11-26 16:45:25 +01:00
}
2021-12-20 17:32:32 +01:00
export async function mount<
T extends abstract new (...args: any) => any = any,
P extends object = any,
E = any
>(
C: T & ComponentConstructor<P, E>,
2021-12-20 17:32:32 +01:00
target: HTMLElement,
config: AppConfig<P, E> & MountOptions = {}
): Promise<Component<P, E> & InstanceType<T>> {
2021-12-20 17:32:32 +01:00
return new App(C, config).mount(target, config);
}