Files
owl/src/component.ts
T

331 lines
9.3 KiB
TypeScript
Raw Normal View History

2019-03-14 11:30:49 +01:00
import h from "../libs/snabbdom/src/h";
import sdAttrs from "../libs/snabbdom/src/modules/attributes";
import sdListeners from "../libs/snabbdom/src/modules/eventlisteners";
import { init } from "../libs/snabbdom/src/snabbdom";
import { VNode } from "../libs/snabbdom/src/vnode";
import { EventBus } from "./event_bus";
2019-03-14 14:29:57 +01:00
import { QWeb } from "./qweb";
2019-03-14 09:03:39 +01:00
import { idGenerator } from "./utils";
let getId = idGenerator();
2019-01-17 18:08:39 +01:00
2019-01-27 22:02:08 +01:00
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
2019-01-31 13:19:30 +01:00
2019-03-14 12:51:37 +01:00
export interface Env {
2019-01-16 13:19:58 +01:00
qweb: QWeb;
}
2019-03-14 12:51:37 +01:00
export interface Meta<T extends Env, Props> {
readonly id: number;
vnode: VNode | null;
isStarted: boolean;
isMounted: boolean;
2019-01-26 18:20:49 +01:00
isDestroyed: boolean;
2019-02-05 14:16:51 +01:00
parent: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, any> };
// children mapping: from templateID to widgetID
2019-01-28 15:51:20 +01:00
// should it be a map number => Widget?
cmap: { [key: number]: number };
2019-02-07 12:46:30 +01:00
renderId: number;
2019-03-12 11:42:46 +01:00
renderProps: Props | null;
2019-02-07 12:46:30 +01:00
renderPromise: Promise<VNode> | null;
boundHandlers: { [key: number]: any };
2019-01-16 13:19:58 +01:00
}
2019-01-27 22:02:08 +01:00
const patch = init([sdListeners, sdAttrs]);
2019-02-02 16:51:24 +01:00
export interface Type<T> extends Function {
new (...args: any[]): T;
}
2019-01-27 22:02:08 +01:00
//------------------------------------------------------------------------------
// Widget
//------------------------------------------------------------------------------
2019-01-31 13:19:30 +01:00
2019-02-05 14:16:51 +01:00
export class Component<
2019-03-14 12:51:37 +01:00
T extends Env,
2019-02-05 08:56:33 +01:00
Props,
State extends {}
> extends EventBus {
2019-03-14 12:51:37 +01:00
readonly __widget__: Meta<Env, Props>;
template: string = "default";
2019-02-03 09:42:35 +01:00
inlineTemplate: string | null = null;
2019-01-16 13:19:58 +01:00
get el(): HTMLElement | null {
2019-01-26 18:23:43 +01:00
return this.__widget__.vnode ? (<any>this).__widget__.vnode.elm : null;
}
2019-01-23 11:12:54 +01:00
env: T;
2019-02-05 08:56:33 +01:00
state: State = <State>{};
2019-01-29 20:49:51 +01:00
props: Props;
2019-02-04 21:09:17 +01:00
refs: {
2019-02-05 14:16:51 +01:00
[key: string]: Component<T, any, any> | HTMLElement | undefined;
2019-02-04 21:09:17 +01:00
} = {};
2019-01-16 13:19:58 +01:00
//--------------------------------------------------------------------------
// Lifecycle
//--------------------------------------------------------------------------
2019-02-05 14:16:51 +01:00
constructor(parent: Component<T, any, any> | T, props?: Props) {
super();
2019-02-03 09:42:35 +01:00
2019-01-29 20:49:51 +01:00
// 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>props;
let id: number;
2019-02-05 14:16:51 +01:00
let p: Component<T, any, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
2019-03-14 09:03:39 +01:00
id = getId();
parent.__widget__.children[id] = this;
} else {
this.env = parent;
2019-03-14 09:03:39 +01:00
id = getId();
2019-01-16 13:19:58 +01:00
}
2019-01-26 18:23:43 +01:00
this.__widget__ = {
id: id,
vnode: null,
isStarted: false,
isMounted: false,
2019-01-26 18:20:49 +01:00
isDestroyed: false,
parent: p,
children: {},
2019-02-07 12:46:30 +01:00
cmap: {},
renderId: 1,
renderPromise: null,
2019-03-12 11:42:46 +01:00
renderProps: props || null,
boundHandlers: {}
};
2019-01-16 13:19:58 +01:00
}
async willStart() {}
mounted() {}
willUnmount() {}
destroyed() {}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
2019-03-11 17:00:42 +01:00
/**
* Attach a child widget to a given html element
*
* This is most of the time not necessary, since widgets should primarily be
* created/managed with the t-widget directive in a qweb template. However,
* for the cases where we need more control, this method will do what is
* necessary to make sure all the proper hooks are called (for example,
* mounted/willUnmount)
*
* Note that this method makes a few assumptions:
* - the child widget is indeed a child of the current widget
* - the target is inside the dom of the current widget (typically a ref)
*/
attachChild(child: Component<T, any, any>, target: HTMLElement) {
target.appendChild(child.el!);
child.__mount();
}
2019-03-19 10:05:46 +01:00
async mount(target: HTMLElement): Promise<void> {
2019-02-07 12:10:44 +01:00
const vnode = await this._start();
if (this.__widget__.isDestroyed) {
// widget was destroyed before we get here...
return;
}
2019-02-07 12:10:44 +01:00
this._patch(vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.visitSubTree(w => {
2019-01-26 18:23:43 +01:00
if (!w.__widget__.isMounted && this.el!.contains(w.el)) {
w.__widget__.isMounted = true;
w.mounted();
2019-01-31 10:55:06 +01:00
return true;
}
2019-01-31 10:55:06 +01:00
return false;
});
2019-01-20 14:46:36 +01:00
}
2019-01-16 13:19:58 +01:00
}
2019-01-31 10:55:06 +01:00
detach() {
if (this.el) {
this.visitSubTree(w => {
if (w.__widget__.isMounted) {
w.willUnmount();
w.__widget__.isMounted = false;
return true;
}
return false;
});
this.el.remove();
}
}
2019-01-16 23:03:51 +01:00
destroy() {
2019-01-26 18:23:43 +01:00
if (!this.__widget__.isDestroyed) {
for (let id in this.__widget__.children) {
this.__widget__.children[id].destroy();
}
2019-01-27 15:40:10 +01:00
if (this.__widget__.isMounted) {
this.willUnmount();
}
2019-01-26 18:20:49 +01:00
if (this.el) {
this.el.remove();
2019-01-27 15:40:10 +01:00
this.__widget__.isMounted = false;
2019-01-26 18:23:43 +01:00
delete this.__widget__.vnode;
2019-01-26 18:20:49 +01:00
}
if (this.__widget__.parent) {
let id = this.__widget__.id;
delete this.__widget__.parent.__widget__.children[id];
this.__widget__.parent = null;
}
this.clear();
2019-01-26 18:23:43 +01:00
this.__widget__.isDestroyed = true;
2019-01-26 18:20:49 +01:00
this.destroyed();
2019-01-16 13:19:58 +01:00
}
}
2019-03-19 10:05:46 +01:00
shouldUpdate(nextProps: Props): boolean {
return true;
}
async updateProps(nextProps: Props): Promise<void> {
if (nextProps === this.__widget__.renderProps) {
await this.__widget__.renderPromise;
return;
}
const shouldUpdate = this.shouldUpdate(nextProps);
return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
}
2019-01-17 18:08:39 +01:00
/**
* This is the safest update method for widget: its job is to update the state
* and rerender (if widget is mounted).
*
* Notes:
* - it checks if we do not add extra keys to the state.
2019-03-19 10:05:46 +01:00
* - it is ok to call updateState before the widget is started. In that
* case, it will simply update the state and will not rerender
2019-01-17 18:08:39 +01:00
*/
2019-03-19 10:05:46 +01:00
async updateState(nextState: Partial<State>) {
2019-02-04 13:23:41 +01:00
if (Object.keys(nextState).length === 0) {
return;
}
2019-02-04 21:09:17 +01:00
Object.assign(this.state, nextState);
2019-01-26 18:23:43 +01:00
if (this.__widget__.isStarted) {
2019-01-30 10:34:30 +01:00
return this.render();
}
2019-01-16 13:19:58 +01:00
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
2019-03-12 11:42:46 +01:00
async _updateProps(nextProps: Props): Promise<void> {
this.props = nextProps;
return this.render();
}
async render(): Promise<void> {
if (this.__widget__.isDestroyed) {
return;
}
2019-02-07 12:46:30 +01:00
const renderVDom = this._render();
const renderId = this.__widget__.renderId;
const vnode = await renderVDom;
if (renderId === this.__widget__.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.
this._patch(vnode);
}
2019-02-07 12:10:44 +01:00
}
2019-03-12 11:42:46 +01:00
_patch(vnode) {
2019-02-07 12:46:30 +01:00
this.__widget__.renderPromise = null;
2019-01-26 18:23:43 +01:00
this.__widget__.vnode = patch(
this.__widget__.vnode || document.createElement(vnode.sel!),
vnode
);
2019-01-16 13:19:58 +01:00
}
2019-03-12 11:42:46 +01:00
async _start(): Promise<VNode> {
2019-02-07 12:46:30 +01:00
this.__widget__.renderProps = this.props;
this.__widget__.renderPromise = this.willStart().then(() => {
if (this.__widget__.isDestroyed) {
return Promise.resolve(h("div"));
2019-02-07 12:46:30 +01:00
}
this.__widget__.isStarted = true;
2019-02-07 12:46:30 +01:00
if (this.inlineTemplate) {
this.env.qweb.addTemplate(
this.inlineTemplate,
this.inlineTemplate,
true
);
}
return this._render();
});
return this.__widget__.renderPromise;
}
2019-02-07 12:10:44 +01:00
async _render(): Promise<VNode> {
2019-02-07 12:46:30 +01:00
this.__widget__.renderId++;
2019-01-25 10:25:42 +01:00
const promises: Promise<void>[] = [];
2019-02-03 09:42:35 +01:00
const template = this.inlineTemplate || this.template;
let vnode = this.env.qweb.render(template, this, {
promises,
handlers: this.__widget__.boundHandlers
});
// 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
2019-01-26 18:23:43 +01:00
vnode.key = this.__widget__.id;
2019-02-07 12:46:30 +01:00
this.__widget__.renderProps = this.props;
this.__widget__.renderPromise = Promise.all(promises).then(() => vnode);
return this.__widget__.renderPromise;
2019-01-25 10:25:42 +01:00
}
/**
* Only called by qweb t-widget directive
*/
2019-01-30 14:56:47 +01:00
_mount(vnode: VNode, elm: HTMLElement): VNode {
this.__widget__.vnode = patch(elm, vnode);
this.__mount();
return this.__widget__.vnode;
}
__mount() {
if (this.__widget__.isMounted) {
return;
}
2019-01-26 18:23:43 +01:00
if (this.__widget__.parent) {
if (this.__widget__.parent!.__widget__.isMounted) {
2019-01-26 18:23:43 +01:00
this.__widget__.isMounted = true;
this.mounted();
const children = this.__widget__.children;
for (let id in children) {
children[id].__mount();
}
}
}
}
2019-03-12 11:42:46 +01:00
visitSubTree(callback: (w: Component<T, any, any>) => boolean) {
2019-01-31 10:55:06 +01:00
const shouldVisitChildren = callback(this);
if (shouldVisitChildren) {
const children = this.__widget__.children;
for (let id in children) {
children[id].visitSubTree(callback);
}
2019-01-21 16:26:51 +01:00
}
}
2019-01-16 13:19:58 +01:00
}