import { EventBus } from "./event_bus"; import { Observer } from "./observer"; import { QWeb } from "./qweb"; import { h, patch, VNode } from "./vdom"; //------------------------------------------------------------------------------ // Types/helpers //------------------------------------------------------------------------------ export interface Env { qweb: QWeb; } interface Meta { readonly id: number; vnode: VNode | null; isStarted: boolean; isMounted: boolean; isDestroyed: boolean; parent: Component | null; children: { [key: number]: Component }; // children mapping: from templateID to widgetID // should it be a map number => Widget? cmap: { [key: number]: number }; renderId: number; renderProps: Props | null; renderPromise: Promise | null; boundHandlers: { [key: number]: any }; observer?: Observer; } //------------------------------------------------------------------------------ // Widget //------------------------------------------------------------------------------ let nextId = 1; export class Component< T extends Env, Props extends {}, State extends {} > extends EventBus { readonly __owl__: Meta; template: string = "default"; inlineTemplate: string | null = null; get el(): HTMLElement | null { return this.__owl__.vnode ? (this).__owl__.vnode.elm : null; } env: T; state?: State; props: Props; refs: { [key: string]: Component | HTMLElement | undefined; } = {}; //-------------------------------------------------------------------------- // Lifecycle //-------------------------------------------------------------------------- /** * Creates an instance of Component. * * The root widget of a component tree needs an environment: * * ```javascript * const root = new RootWidget(env, props); * ``` * * Every other widget simply needs a reference to its parent: * * ```javascript * const child = new SomeWidget(parent, props); * ``` * * Note that most of the time, only the root widget needs to be created by * hand. Other widgets should be created automatically by the framework (with * the t-widget directive in a template) */ constructor(parent: Component | T, props?: Props) { super(); // is this a good idea? // Pro: if props is empty, we can create easily a widget // Con: this is not really safe // Pro: but creating widget (by a template) is always unsafe anyway this.props = props || {}; let id: number = nextId++; let p: Component | null = null; if (parent instanceof Component) { p = parent; this.env = parent.env; parent.__owl__.children[id] = this; } else { this.env = parent; } this.__owl__ = { id: id, vnode: null, isStarted: false, isMounted: false, isDestroyed: false, parent: p, children: {}, cmap: {}, renderId: 1, renderPromise: null, renderProps: props || null, boundHandlers: {} }; } /** * willStart is an asynchronous hook that can be implemented to perform some * action before the initial rendering of a component. * * It will be called exactly once before the initial rendering. It is useful * in some cases, for example, to load external assets (such as a JS library) * before the widget is rendered. * * Note that a slow willStart method will slow down the rendering of the user * interface. Therefore, some effort should be made to make this method as * fast as possible. * * Note: this method should not be called manually. */ async willStart() {} /** * mounted is a hook that is called each time a component is attached to the * DOM. This is a good place to add some listeners, or to interact with the * DOM, if the component needs to perform some measure for example. * * Note: this method should not be called manually. * * @see willUnmount */ mounted() {} /** * The willUpdateProps is an asynchronous hook, called just before new props * are set. This is useful if the component needs some asynchronous task * performed, depending on the props (for example, assuming that the props are * some record Id, fetching the record data). * * This hook is not called during the first render (but willStart is called * and performs a similar job). */ async willUpdateProps(nextProps: Props) {} /** * The willPatch hook is called just before the DOM patching process starts. * It is not called on the initial render. This is useful to get some * information which are in the DOM. For example, the current position of the * scrollbar * * The return value of willPatch will be given to the patched function. */ willPatch(): any {} /** * This hook is called whenever a component did actually update its props, * state or env. * * This method is not called on the initial render. It is useful to interact * with the DOM (for example, through an external library) whenever the * component was updated. * * Updating the widget state in this hook is possible, but not encouraged. * One need to be careful, because updates here will cause rerender, which in * turn will cause other calls to updated. So, we need to be particularly * careful at avoiding endless cycles. * * The snapshot parameter is the result of the call to willPatch. */ patched(snapshot: any) {} /** * willUnmount is a hook that is called each time just before a component is * unmounted from the DOM. This is a good place to remove some listeners, for * example. * * Note: this method should not be called manually. * * @see mounted */ willUnmount() {} //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- async mount(target: HTMLElement): Promise { const vnode = await this._prepare(); if (this.__owl__.isDestroyed) { // widget was destroyed before we get here... return; } this._patch(vnode); target.appendChild(this.el!); if (document.body.contains(target)) { this._callMounted(); } } _callMounted() { const children = this.__owl__.children; for (let id in children) { const comp = children[id]; if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) { comp._callMounted(); } } this.__owl__.isMounted = true; this.mounted(); } _callWillUnmount() { this.willUnmount(); this.__owl__.isMounted = false; const children = this.__owl__.children; for (let id in children) { const comp = children[id]; if (comp.__owl__.isMounted) { comp._callWillUnmount(); } } } unmount() { if (this.__owl__.isMounted) { this._callWillUnmount(); this.el!.remove(); } } async render(force: boolean = false, patchQueue?: any[]): Promise { if (this.__owl__.isDestroyed) { return; } const shouldCallPatchHooks: boolean = !patchQueue; if (shouldCallPatchHooks) { patchQueue = []; } const renderVDom = this._render(force, patchQueue); const renderId = this.__owl__.renderId; const vnode = await renderVDom; if (renderId === this.__owl__.renderId) { // we only update the vnode and the actual DOM if no other rendering // occurred between now and when the render method was initially called. if (shouldCallPatchHooks) { for (let i = 0; i < patchQueue!.length; i++) { const c = patchQueue![i]; c.__owl__.willPatchVal = c.willPatch(); } } this._patch(vnode); if (shouldCallPatchHooks) { for (let i = patchQueue!.length - 1; i >= 0; i--) { const c = patchQueue![i]; c.patched(c.__owl__.willPatchVal); } } } } destroy() { if (!this.__owl__.isDestroyed) { const el = this.el; this._destroy(this.__owl__.parent); if (el) { el.remove(); } } } _destroy(parent) { const isMounted = this.__owl__.isMounted; if (isMounted) { this.willUnmount(); this.__owl__.isMounted = false; } const children = Object.values(this.__owl__.children); for (let child of children) { child._destroy(this); } if (parent) { let id = this.__owl__.id; delete parent.__owl__.children[id]; this.__owl__.parent = null; } this.clear(); this.__owl__.isDestroyed = true; delete this.__owl__.vnode; } shouldUpdate(nextProps: Props): boolean { return true; } /** * This method is the correct way to update the environment of a widget. Doing * this will cause a full rerender of the widget and its children, so this is * an operation that should not be done frequently. * * A good usecase for updating the environment would be to update some mostly * static config keys, such as a boolean to determine if we are in mobile * mode or not. */ async updateEnv(nextEnv: Partial): Promise { if (this.__owl__.parent && this.__owl__.parent.env === this.env) { this.env = Object.create(this.env); } Object.assign(this.env, nextEnv); if (this.__owl__.isMounted) { await this.render(true); } } set(target: any, key: string | number, value: any) { this.__owl__.observer!.set(target, key, value); } //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- async _updateProps( nextProps: Props, forceUpdate: boolean = false, patchQueue: any[] ): Promise { const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); if (shouldUpdate) { await this.willUpdateProps(nextProps); this.props = nextProps; await this.render(false, patchQueue); } } _patch(vnode) { this.__owl__.renderPromise = null; if (this.__owl__.vnode) { this.__owl__.vnode = patch(this.__owl__.vnode, vnode); } else { this.__owl__.vnode = patch(document.createElement(vnode.sel!), vnode); } } async _prepare(): Promise { this.__owl__.renderProps = this.props; this.__owl__.renderPromise = this.willStart().then(() => { if (this.__owl__.isDestroyed) { return Promise.resolve(h("div")); } this.__owl__.isStarted = true; if (this.inlineTemplate) { this.env.qweb.addTemplate( this.inlineTemplate, this.inlineTemplate, true ); } this._observeState(); return this._render(); }); return this.__owl__.renderPromise; } async _render( force: boolean = false, patchQueue: any[] = [] ): Promise { if (this.__owl__.isMounted) { patchQueue.push(this); } this.__owl__.renderId++; const promises: Promise[] = []; const template = this.inlineTemplate || this.template; if (this.__owl__.observer) { this.__owl__.observer.allowMutations = false; } let vnode = this.env.qweb.render(template, this, { promises, handlers: this.__owl__.boundHandlers, forceUpdate: force, patchQueue }); if (this.__owl__.observer) { this.__owl__.observer.allowMutations = true; } // this part is critical for the patching process to be done correctly. The // tricky part is that a child widget can be rerendered on its own, which // will update its own vnode representation without the knowledge of the // parent widget. With this, we make sure that the parent widget will be // able to patch itself properly after vnode.key = this.__owl__.id; this.__owl__.renderProps = this.props; this.__owl__.renderPromise = Promise.all(promises).then(() => vnode); return this.__owl__.renderPromise; } /** * Only called by qweb t-widget directive */ _mount(vnode: VNode, elm: HTMLElement): VNode { this.__owl__.vnode = patch(elm, vnode); if ( this.__owl__.parent && this.__owl__.parent.__owl__.isMounted && !this.__owl__.isMounted ) { this._callMounted(); } return this.__owl__.vnode; } __mount() { if (!this.__owl__.isMounted) { this.__owl__.isMounted = true; this.mounted(); } } _observeState() { if (this.state) { this.__owl__.observer = new Observer(); this.__owl__.observer.observe(this.state); this.__owl__.observer.notifyCB = this.render.bind(this); } } }