Files
owl/src/component/component_node.ts
T

262 lines
7.3 KiB
TypeScript
Raw Normal View History

import type { App, Env } from "../app/app";
2020-11-26 16:45:25 +01:00
import { BDom, VNode } from "../blockdom";
import { Component } from "./component";
import {
Fiber,
makeChildFiber,
makeRootFiber,
MountFiber,
MountOptions,
2020-11-26 16:45:25 +01:00
RootFiber,
} from "./fibers";
import { handleError, fibersInError } from "./error_handling";
2021-10-22 11:56:56 +02:00
import { applyDefaultProps } from "./props_validation";
2020-11-26 16:45:25 +01:00
import { STATUS } from "./status";
2021-11-03 10:00:15 +01:00
import { applyStyles } from "./style";
2020-11-26 16:45:25 +01:00
export function component(
name: string | typeof Component,
props: any,
key: string,
ctx: ComponentNode,
parent: any
): ComponentNode {
let node: any = ctx.children[key];
let isDynamic = typeof name !== "string";
if (node) {
if (node.status < STATUS.MOUNTED) {
node.destroy();
node = undefined;
} else if (node.status === STATUS.DESTROYED) {
node = undefined;
}
2020-11-26 16:45:25 +01:00
}
if (isDynamic && node && node.component.constructor !== name) {
node = undefined;
}
const parentFiber = ctx.fiber!;
if (node) {
node.updateAndRender(props, parentFiber);
} else {
// new component
let C;
if (isDynamic) {
C = name;
} else {
C = parent.constructor.components[name as any];
if (!C) {
throw new Error(`Cannot find the definition of component "${name}"`);
}
}
2021-11-02 13:53:44 +01:00
node = new ComponentNode(C, props, ctx.app, ctx);
2020-11-26 16:45:25 +01:00
ctx.children[key] = node;
const fiber = makeChildFiber(node, parentFiber);
node.initiateRender(fiber);
}
return node;
}
// -----------------------------------------------------------------------------
// Component VNode
// -----------------------------------------------------------------------------
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = typeof Component>
implements VNode<ComponentNode>
{
2020-11-26 16:45:25 +01:00
el?: HTMLElement | Text | undefined;
app: App;
fiber: Fiber | null = null;
component: InstanceType<T>;
bdom: BDom | null = null;
status: STATUS = STATUS.NEW;
renderFn: Function;
2021-11-02 13:53:44 +01:00
parent: ComponentNode | null;
level: number;
childEnv: Env;
2020-11-26 16:45:25 +01:00
children: { [key: string]: ComponentNode } = Object.create(null);
refs: any = {};
willStart: LifecycleHook[] = [];
willUpdateProps: LifecycleHook[] = [];
willUnmount: LifecycleHook[] = [];
mounted: LifecycleHook[] = [];
willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = [];
2020-11-26 16:45:25 +01:00
2021-11-02 13:53:44 +01:00
constructor(C: T, props: any, app: App, parent?: ComponentNode) {
2020-11-26 16:45:25 +01:00
currentNode = this;
this.app = app;
2021-11-02 13:53:44 +01:00
this.parent = parent || null;
this.level = parent ? parent.level + 1 : 0;
2021-10-22 11:56:56 +02:00
applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
2021-11-03 10:00:15 +01:00
if (C.style) {
applyStyles(C);
}
2020-11-26 16:45:25 +01:00
this.component.setup();
}
mountComponent(target: any, options?: MountOptions): Promise<InstanceType<T>> {
const fiber = new MountFiber(this, target, options);
2020-11-26 16:45:25 +01:00
this.app.scheduler.addFiber(fiber);
this.initiateRender(fiber);
return fiber.promise.then(() => this.component);
}
async initiateRender(fiber: Fiber | MountFiber) {
2021-11-25 11:13:48 +01:00
this.fiber = fiber;
2020-11-26 16:45:25 +01:00
if (this.mounted.length) {
fiber.root.mounted.push(fiber);
}
const component = this.component;
try {
await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) {
handleError({ node: this, error: e });
return;
}
2020-11-26 16:45:25 +01:00
if (this.status === STATUS.NEW && this.fiber === fiber) {
this._render(fiber);
}
}
async render() {
2021-11-25 10:47:50 +01:00
const current = this.fiber;
if (current && !current.bdom && !fibersInError.has(current)) {
return current.root.promise;
2020-11-26 16:45:25 +01:00
}
2021-11-25 10:47:50 +01:00
if (!this.bdom && !current) {
2020-11-26 16:45:25 +01:00
// should find a way to return the future mounting promise
return;
}
2021-11-25 10:47:50 +01:00
const fiber = makeRootFiber(this);
2021-11-25 11:13:48 +01:00
this.fiber = fiber;
2020-11-26 16:45:25 +01:00
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status === STATUS.DESTROYED) {
return;
}
2021-11-25 10:47:50 +01:00
// We only want to actually render the component if the following two
// conditions are true:
// * this.fiber: it could be null, in which case the render has been cancelled
// * (current || !fiber.parent): if current is not null, this means that the
// render function was called when a render was already occurring. In this
// case, the pending rendering was cancelled, and the fiber needs to be
// rendered to complete the work. If current is null, we check that the
// fiber has no parent. If that is the case, the fiber was downgraded from
// a root fiber to a child fiber in the previous microtick, because it was
// embedded in a rendering coming from above, so the fiber will be rendered
// in the next microtick anyway, so we should not render it again.
if (this.fiber && (current || !fiber.parent)) {
2020-11-26 16:45:25 +01:00
this._render(fiber);
}
return fiber.root.promise;
}
_render(fiber: Fiber | RootFiber) {
try {
fiber.bdom = this.renderFn();
fiber.root.counter--;
2020-11-26 16:45:25 +01:00
} catch (e) {
handleError({ node: this, error: e });
2020-11-26 16:45:25 +01:00
}
}
destroy() {
let shouldRemove = this.status === STATUS.MOUNTED;
this._destroy();
if (shouldRemove) {
2020-11-26 16:45:25 +01:00
this.bdom!.remove();
}
}
2020-11-26 16:45:25 +01:00
_destroy() {
const component = this.component;
if (this.status === STATUS.MOUNTED) {
for (let cb of this.willUnmount) {
2020-11-26 16:45:25 +01:00
cb.call(component);
}
}
for (let child of Object.values(this.children)) {
child._destroy();
2020-11-26 16:45:25 +01:00
}
for (let cb of this.willDestroy) {
cb.call(component);
}
this.status = STATUS.DESTROYED;
2020-11-26 16:45:25 +01:00
}
async updateAndRender(props: any, parentFiber: Fiber) {
// update
const fiber = makeChildFiber(this, parentFiber);
2021-11-25 11:13:48 +01:00
this.fiber = fiber;
2020-11-26 16:45:25 +01:00
if (this.willPatch.length) {
parentFiber.root.willPatch.push(fiber);
}
if (this.patched.length) {
parentFiber.root.patched.push(fiber);
}
const component = this.component;
2021-10-22 11:56:56 +02:00
applyDefaultProps(props, component.constructor as any);
2020-11-26 16:45:25 +01:00
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
if (fiber !== this.fiber) {
return;
}
this.component.props = props;
this._render(fiber);
}
// ---------------------------------------------------------------------------
// Block DOM methods
// ---------------------------------------------------------------------------
firstNode(): Node | undefined {
const bdom = this.bdom;
return bdom ? bdom.firstNode() : undefined;
}
mount(parent: HTMLElement, anchor: ChildNode) {
const bdom = this.fiber!.bdom!;
this.bdom = bdom;
bdom.mount(parent, anchor);
this.status = STATUS.MOUNTED;
this.fiber!.appliedToDom = true;
this.fiber = null;
}
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
this.bdom!.moveBefore(other ? other.bdom : null, afterNode);
}
patch() {
this.bdom!.patch(this!.fiber!.bdom!, false);
this.fiber!.appliedToDom = true;
this.fiber = null;
}
beforeRemove() {
this._destroy();
2020-11-26 16:45:25 +01:00
}
remove() {
this.bdom!.remove();
}
}