import type { App, Env } from "./app"; import { BDom, VNode } from "./blockdom"; import { Component, ComponentConstructor, Props } from "./component"; import { fibersInError, OwlError } from "./error_handling"; import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers"; import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity"; import { STATUS } from "./status"; import { batched, Callback } from "./utils"; let currentNode: ComponentNode | null = null; export function getCurrent(): ComponentNode { if (!currentNode) { throw new OwlError("No active component (a hook function should only be called in 'setup')"); } return currentNode; } export function useComponent(): Component { return currentNode!.component; } /** * Apply default props (only top level). */ function applyDefaultProps

(props: P, defaultProps: Partial

) { for (let propName in defaultProps) { if (props[propName] === undefined) { (props as any)[propName] = defaultProps[propName]; } } } // ----------------------------------------------------------------------------- // Integration with reactivity system (useState) // ----------------------------------------------------------------------------- const batchedRenderFunctions = new WeakMap(); /** * Creates a reactive object that will be observed by the current component. * Reading data from the returned object (eg during rendering) will cause the * component to subscribe to that data and be rerendered when it changes. * * @param state the state to observe * @returns a reactive object that will cause the component to re-render on * relevant changes * @see reactive */ export function useState(state: T): T { const node = getCurrent(); let render = batchedRenderFunctions.get(node)!; if (!render) { render = batched(node.render.bind(node, false)); batchedRenderFunctions.set(node, render); // manual implementation of onWillDestroy to break cyclic dependency node.willDestroy.push(clearReactivesForCallback.bind(null, render)); } return reactive(state, render); } // ----------------------------------------------------------------------------- // Component VNode class // ----------------------------------------------------------------------------- type LifecycleHook = Function; export class ComponentNode

implements VNode> { el?: HTMLElement | Text | undefined; app: App; fiber: Fiber | null = null; component: Component; bdom: BDom | null = null; status: STATUS = STATUS.NEW; forceNextRender: boolean = false; parentKey: string | null; props: P; nextProps: P | null = null; renderFn: Function; parent: ComponentNode | null; childEnv: Env; children: { [key: string]: ComponentNode } = Object.create(null); refs: any = {}; willStart: LifecycleHook[] = []; willUpdateProps: LifecycleHook[] = []; willUnmount: LifecycleHook[] = []; mounted: LifecycleHook[] = []; willPatch: LifecycleHook[] = []; patched: LifecycleHook[] = []; willDestroy: LifecycleHook[] = []; constructor( C: ComponentConstructor, props: P, app: App, parent: ComponentNode | null, parentKey: string | null ) { currentNode = this; this.app = app; this.parent = parent; this.props = props; this.parentKey = parentKey; const defaultProps = C.defaultProps; props = Object.assign({}, props); if (defaultProps) { applyDefaultProps(props, defaultProps); } const env = (parent && parent.childEnv) || app.env; this.childEnv = env; for (const key in props) { const prop = props[key]; if (prop && typeof prop === "object" && targets.has(prop)) { props[key] = useState(prop); } } this.component = new C(props, env, this); const ctx = Object.assign(Object.create(this.component), { this: this.component }); this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this); this.component.setup(); currentNode = null; } mountComponent(target: any, options?: MountOptions) { const fiber = new MountFiber(this, target, options); this.app.scheduler.addFiber(fiber); this.initiateRender(fiber); } async initiateRender(fiber: Fiber | MountFiber) { this.fiber = fiber; 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) { this.app.handleError({ node: this, error: e }); return; } if (this.status === STATUS.NEW && this.fiber === fiber) { fiber.render(); } } async render(deep: boolean) { if (this.status >= STATUS.CANCELLED) { return; } let current = this.fiber; if (current && (current.root!.locked || (current as any).bdom === true)) { await Promise.resolve(); // situation may have changed after the microtask tick current = this.fiber; } if (current) { if (!current.bdom && !fibersInError.has(current)) { if (deep) { // we want the render from this point on to be with deep=true current.deep = deep; } return; } // if current rendering was with deep=true, we want this one to be the same deep = deep || current.deep; } else if (!this.bdom) { return; } const fiber = makeRootFiber(this); fiber.deep = deep; this.fiber = fiber; this.app.scheduler.addFiber(fiber); await Promise.resolve(); if (this.status >= STATUS.CANCELLED) { return; } // 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 === fiber && (current || !fiber.parent)) { fiber.render(); } } cancel() { this._cancel(); delete this.parent!.children[this.parentKey!]; this.app.scheduler.scheduleDestroy(this); } _cancel() { this.status = STATUS.CANCELLED; const children = this.children; for (let childKey in children) { children[childKey]._cancel(); } } destroy() { let shouldRemove = this.status === STATUS.MOUNTED; this._destroy(); if (shouldRemove) { this.bdom!.remove(); } } _destroy() { const component = this.component; if (this.status === STATUS.MOUNTED) { for (let cb of this.willUnmount) { cb.call(component); } } for (let child of Object.values(this.children)) { child._destroy(); } if (this.willDestroy.length) { try { for (let cb of this.willDestroy) { cb.call(component); } } catch (e) { this.app.handleError({ error: e, node: this }); } } this.status = STATUS.DESTROYED; } async updateAndRender(props: P, parentFiber: Fiber) { this.nextProps = props; props = Object.assign({}, props); // update const fiber = makeChildFiber(this, parentFiber); this.fiber = fiber; const component = this.component; const defaultProps = (component.constructor as any).defaultProps; if (defaultProps) { applyDefaultProps(props, defaultProps); } currentNode = this; for (const key in props) { const prop = props[key]; if (prop && typeof prop === "object" && targets.has(prop)) { props[key] = useState(prop); } } currentNode = null; const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props))); await prom; if (fiber !== this.fiber) { return; } component.props = props; fiber.render(); const parentRoot = parentFiber.root!; if (this.willPatch.length) { parentRoot.willPatch.push(fiber); } if (this.patched.length) { parentRoot.patched.push(fiber); } } /** * Finds a child that has dom that is not yet updated, and update it. This * method is meant to be used only in the context of repatching the dom after * a mounted hook failed and was handled. */ updateDom() { if (!this.fiber) { return; } if (this.bdom === this.fiber!.bdom) { // If the error was handled by some child component, we need to find it to // apply its change for (let k in this.children) { const child = this.children[k]; child.updateDom(); } } else { // if we get here, this is the component that handled the error and rerendered // itself, so we can simply patch the dom this.bdom!.patch(this.fiber!.bdom, false); this.fiber!.appliedToDom = true; this.fiber = null; } } /** * Sets a ref to a given HTMLElement. * * @param name the name of the ref to set * @param el the HTMLElement to set the ref to. The ref is not set if the el * is null, but useRef will not return elements that are not in the DOM */ setRef(name: string, el: HTMLElement | null) { if (el) { this.refs[name] = el; } } // --------------------------------------------------------------------------- // 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.children = this.fiber!.childrenMap; this.fiber = null; } moveBeforeDOMNode(node: Node | null, parent?: HTMLElement): void { this.bdom!.moveBeforeDOMNode(node, parent); } moveBeforeVNode(other: ComponentNode | null, afterNode: Node | null) { this.bdom!.moveBeforeVNode(other ? other.bdom : null, afterNode); } patch() { if (this.fiber && this.fiber.parent) { // we only patch here renderings coming from above. renderings initiated // by the component will be patched independently in the appropriate // fiber.complete this._patch(); this.props = this.nextProps!; } } _patch() { let hasChildren = false; // eslint-disable-next-line @typescript-eslint/no-unused-vars for (let _k in this.children) { hasChildren = true; break; } const fiber = this.fiber!; this.children = fiber.childrenMap; this.bdom!.patch(fiber.bdom!, hasChildren); fiber.appliedToDom = true; this.fiber = null; } beforeRemove() { this._destroy(); } remove() { this.bdom!.remove(); } // --------------------------------------------------------------------------- // Some debug helpers // --------------------------------------------------------------------------- get name(): string { return this.component.constructor.name; } get subscriptions(): ReturnType { const render = batchedRenderFunctions.get(this); return render ? getSubscriptions(render) : []; } }