2019-01-22 19:25:12 +01:00
|
|
|
import sdAttrs from "../../../libs/snabbdom/src/modules/attributes";
|
2019-01-23 14:28:41 +01:00
|
|
|
import sdListeners from "../../../libs/snabbdom/src/modules/eventlisteners";
|
|
|
|
|
import { init } from "../../../libs/snabbdom/src/snabbdom";
|
2019-01-22 19:25:12 +01:00
|
|
|
import { VNode } from "../../../libs/snabbdom/src/vnode";
|
2019-01-24 10:33:11 +01:00
|
|
|
import { QWeb } from "./qweb_vdom";
|
2019-02-02 18:17:21 +01:00
|
|
|
import { EventBus } from "./event_bus";
|
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-01-23 14:28:41 +01:00
|
|
|
export interface WEnv {
|
2019-01-16 13:19:58 +01:00
|
|
|
qweb: QWeb;
|
2019-01-25 14:27:16 +01:00
|
|
|
getID(): number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let wl: any[] = [];
|
|
|
|
|
(<any>window).wl = wl;
|
|
|
|
|
|
|
|
|
|
interface Meta<T extends WEnv> {
|
2019-01-27 09:23:48 +01:00
|
|
|
readonly id: number;
|
2019-01-25 14:27:16 +01:00
|
|
|
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> };
|
2019-01-27 22:16:00 +01:00
|
|
|
// children mapping: from templateID to widgetID
|
2019-01-28 15:51:20 +01:00
|
|
|
// should it be a map number => Widget?
|
2019-01-27 22:16:00 +01:00
|
|
|
cmap: { [key: number]: number };
|
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-02-05 08:56:33 +01:00
|
|
|
T extends WEnv,
|
|
|
|
|
Props,
|
|
|
|
|
State extends {}
|
|
|
|
|
> extends EventBus {
|
2019-01-26 18:23:43 +01:00
|
|
|
__widget__: Meta<WEnv>;
|
2019-02-02 11:41:19 +01:00
|
|
|
template: string = "default";
|
2019-02-03 09:42:35 +01:00
|
|
|
inlineTemplate: string | null = null;
|
2019-01-16 13:19:58 +01:00
|
|
|
|
2019-01-26 08:40:09 +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-26 08:40:09 +01:00
|
|
|
}
|
|
|
|
|
|
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) {
|
2019-02-02 18:17:21 +01:00
|
|
|
super();
|
2019-01-25 14:27:16 +01:00
|
|
|
wl.push(this);
|
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;
|
2019-01-27 13:50:53 +01:00
|
|
|
let id: number;
|
2019-02-05 14:16:51 +01:00
|
|
|
let p: Component<T, any, any> | null = null;
|
|
|
|
|
if (parent instanceof Component) {
|
2019-01-25 14:27:16 +01:00
|
|
|
p = parent;
|
2019-01-26 20:33:23 +01:00
|
|
|
this.env = parent.env;
|
2019-01-27 13:50:53 +01:00
|
|
|
id = this.env.getID();
|
|
|
|
|
parent.__widget__.children[id] = this;
|
2019-01-22 10:38:44 +01:00
|
|
|
} else {
|
|
|
|
|
this.env = parent;
|
2019-01-27 13:50:53 +01:00
|
|
|
id = this.env.getID();
|
2019-01-16 13:19:58 +01:00
|
|
|
}
|
2019-01-26 18:23:43 +01:00
|
|
|
this.__widget__ = {
|
2019-01-27 13:50:53 +01:00
|
|
|
id: id,
|
2019-01-25 14:27:16 +01:00
|
|
|
vnode: null,
|
|
|
|
|
isStarted: false,
|
|
|
|
|
isMounted: false,
|
2019-01-26 18:20:49 +01:00
|
|
|
isDestroyed: false,
|
2019-01-25 14:27:16 +01:00
|
|
|
parent: p,
|
2019-01-27 22:16:00 +01:00
|
|
|
children: {},
|
|
|
|
|
cmap: {}
|
2019-01-25 14:27:16 +01:00
|
|
|
};
|
2019-01-16 13:19:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async willStart() {}
|
|
|
|
|
|
|
|
|
|
mounted() {}
|
|
|
|
|
|
2019-01-28 21:32:21 +01:00
|
|
|
shouldUpdate(nextProps: Props): boolean {
|
2019-01-28 16:16:22 +01:00
|
|
|
return true;
|
|
|
|
|
}
|
2019-01-27 18:45:08 +01:00
|
|
|
|
2019-01-16 13:19:58 +01:00
|
|
|
willUnmount() {}
|
|
|
|
|
|
|
|
|
|
destroyed() {}
|
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
|
// Public
|
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
|
|
2019-01-27 21:50:22 +01:00
|
|
|
async mount(target: HTMLElement): Promise<void> {
|
2019-01-25 11:12:20 +01:00
|
|
|
await this._start();
|
2019-01-27 21:50:22 +01:00
|
|
|
await this.render();
|
|
|
|
|
if (!this.el) {
|
|
|
|
|
// widget was destroyed before we get here...
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-01-25 11:12:20 +01:00
|
|
|
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;
|
2019-01-25 11:12:20 +01:00
|
|
|
w.mounted();
|
2019-01-31 10:55:06 +01:00
|
|
|
return true;
|
2019-01-25 11:12:20 +01:00
|
|
|
}
|
2019-01-31 10:55:06 +01:00
|
|
|
return false;
|
2019-01-25 11:12:20 +01:00
|
|
|
});
|
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) {
|
2019-01-27 21:03:13 +01:00
|
|
|
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
|
|
|
}
|
2019-01-27 13:50:53 +01:00
|
|
|
if (this.__widget__.parent) {
|
|
|
|
|
let id = this.__widget__.id;
|
|
|
|
|
delete this.__widget__.parent.__widget__.children[id];
|
|
|
|
|
this.__widget__.parent = null;
|
|
|
|
|
}
|
2019-02-02 18:17:21 +01:00
|
|
|
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-01-17 18:08:39 +01:00
|
|
|
/**
|
2019-01-30 11:29:54 +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.
|
|
|
|
|
* - it is ok to call updateState before the widget is started. In that
|
2019-01-24 13:48:31 +01:00
|
|
|
* case, it will simply update the state and will not rerender
|
2019-01-17 18:08:39 +01:00
|
|
|
*/
|
2019-02-04 21:09:17 +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-24 13:48:31 +01:00
|
|
|
}
|
2019-01-16 13:19:58 +01:00
|
|
|
}
|
|
|
|
|
|
2019-01-28 21:32:21 +01:00
|
|
|
updateProps(nextProps: Props): Promise<void> {
|
2019-01-28 16:16:22 +01:00
|
|
|
const shouldUpdate = this.shouldUpdate(nextProps);
|
|
|
|
|
this.props = nextProps;
|
|
|
|
|
return shouldUpdate ? this.render() : Promise.resolve();
|
2019-01-27 22:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2019-01-16 13:19:58 +01:00
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
|
// Private
|
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
|
|
2019-01-27 21:50:22 +01:00
|
|
|
async render(): Promise<void> {
|
|
|
|
|
if (this.__widget__.isDestroyed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-01-25 10:25:42 +01:00
|
|
|
const vnode = await this._render();
|
2019-01-26 18:23:43 +01:00
|
|
|
this.__widget__.vnode = patch(
|
|
|
|
|
this.__widget__.vnode || document.createElement(vnode.sel!),
|
2019-01-26 08:40:09 +01:00
|
|
|
vnode
|
|
|
|
|
);
|
2019-01-16 13:19:58 +01:00
|
|
|
}
|
2019-01-21 16:26:51 +01:00
|
|
|
|
2019-01-25 11:12:20 +01:00
|
|
|
private async _start(): Promise<void> {
|
|
|
|
|
await this.willStart();
|
2019-01-27 21:50:22 +01:00
|
|
|
if (!this.__widget__.isDestroyed) {
|
|
|
|
|
this.__widget__.isStarted = true;
|
|
|
|
|
}
|
2019-02-03 09:42:35 +01:00
|
|
|
if (this.inlineTemplate) {
|
|
|
|
|
this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true);
|
|
|
|
|
}
|
2019-01-25 11:12:20 +01:00
|
|
|
}
|
|
|
|
|
|
2019-01-25 10:25:42 +01:00
|
|
|
private async _render(): Promise<VNode> {
|
|
|
|
|
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 });
|
2019-01-26 08:40:09 +01:00
|
|
|
|
|
|
|
|
// 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-01-25 10:25:42 +01:00
|
|
|
return Promise.all(promises).then(() => vnode);
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-26 08:40:09 +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);
|
2019-01-31 15:14:26 +01:00
|
|
|
this.__mount();
|
|
|
|
|
return this.__widget__.vnode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
__mount() {
|
|
|
|
|
if (this.__widget__.isMounted) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-01-26 18:23:43 +01:00
|
|
|
if (this.__widget__.parent) {
|
2019-01-31 15:14:26 +01:00
|
|
|
if (this.__widget__.parent!.__widget__.isMounted) {
|
2019-01-26 18:23:43 +01:00
|
|
|
this.__widget__.isMounted = true;
|
2019-01-25 11:12:20 +01:00
|
|
|
this.mounted();
|
2019-01-31 15:14:26 +01:00
|
|
|
const children = this.__widget__.children;
|
|
|
|
|
for (let id in children) {
|
|
|
|
|
children[id].__mount();
|
|
|
|
|
}
|
2019-01-25 11:12:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-05 14:16:51 +01:00
|
|
|
private 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
|
|
|
}
|