[FIX] component: cancel previous mounting operations if necessary

closes #626
This commit is contained in:
Géry Debongnie
2020-02-16 09:46:47 +01:00
committed by aab-odoo
parent 03585d8fea
commit f7728b93bd
8 changed files with 156 additions and 61 deletions
+17 -29
View File
@@ -37,8 +37,10 @@ export interface Env {
[key: string]: any;
}
export type MountPosition = "first-child" | "last-child" | "self";
interface MountOptions {
position?: "first-child" | "last-child" | "self";
position?: MountPosition;
}
/**
@@ -106,7 +108,6 @@ export class Component<Props extends {} = any, T extends Env = Env> {
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
__target: HTMLElement | undefined;
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -311,21 +312,20 @@ export class Component<Props extends {} = any, T extends Env = Env> {
return Promise.resolve();
}
}
if (__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber;
if (currentFiber.target === target && currentFiber.position === position) {
return scheduler.addFiber(currentFiber);
} else {
scheduler.rejectFiber(currentFiber, "Mounting operation cancelled");
}
}
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
let inserter =
position === "last-child"
? el => target.appendChild(el)
: position === "first-child"
? el => target.prepend(el)
: el => {};
if (position === "self") {
this.__target = target as HTMLElement;
}
const fiber = new Fiber(null, this, false, inserter);
const fiber = new Fiber(null, this, false, target, position);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {});
@@ -370,7 +370,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const isMounted = __owl__.isMounted;
const fiber = new Fiber(null, this, force, null);
const fiber = new Fiber(null, this, force, null, null);
Promise.resolve().then(() => {
if (__owl__.isMounted || !isMounted) {
if (fiber.isCompleted) {
@@ -527,7 +527,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
@@ -559,20 +559,8 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode: VNode) {
const __owl__ = this.__owl__;
if (this.__target) {
if (this.__target.tagName.toLowerCase() !== vnode.sel) {
throw new Error(
`Cannot attach '${this.constructor.name}' to target node (not same tag name)`
);
}
__owl__.vnode = patch(this.__target, vnode);
delete this.__target;
} else {
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
__patch(target: HTMLElement | VNode | DocumentFragment, vnode: VNode) {
this.__owl__.vnode = patch(target as any, vnode);
}
/**
@@ -582,7 +570,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
+1 -1
View File
@@ -368,7 +368,7 @@ QWeb.addDirective({
if (transition) {
ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`);
ctx.addLine(
`w${componentID}.__patch = fiber => {__patch${componentID}.call(w${componentID}, fiber); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
`w${componentID}.__patch = (t, vn) => {__patch${componentID}.call(w${componentID}, t, vn); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
);
}
ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
+44 -12
View File
@@ -1,5 +1,5 @@
import { h, VNode } from "../vdom/index";
import { Component } from "./component";
import { Component, MountPosition } from "./component";
import { scheduler } from "./scheduler";
/**
@@ -46,7 +46,8 @@ export class Fiber {
// scheduler.
counter: number = 0;
inserter: (el: HTMLElement) => void | null;
target: HTMLElement | DocumentFragment | null;
position: MountPosition | null;
scope: any;
@@ -61,10 +62,17 @@ export class Fiber {
error?: Error;
constructor(parent: Fiber | null, component: Component, force: boolean, inserter) {
constructor(
parent: Fiber | null,
component: Component,
force: boolean,
target: HTMLElement | DocumentFragment | null,
position: MountPosition | null
) {
this.component = component;
this.force = force;
this.inserter = inserter;
this.target = target;
this.position = position;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
@@ -179,7 +187,7 @@ export class Fiber {
complete() {
let component = this.component;
this.isCompleted = true;
if (!this.inserter && !component.__owl__.isMounted) {
if (!this.target && !component.__owl__.isMounted) {
return;
}
@@ -208,17 +216,41 @@ export class Fiber {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode!);
if (!fiber.shouldPatch && (!fiber.inserter || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
if (fiber.target && i === 0) {
let target;
if (fiber.position === "self") {
target = fiber.target;
if ((target as HTMLElement).tagName.toLowerCase() !== fiber.vnode!.sel) {
throw new Error(
`Cannot attach '${component.constructor.name}' to target node (not same tag name)`
);
}
} else {
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
}
component.__patch(target!, fiber.vnode!);
} else {
if (fiber.shouldPatch) {
component.__patch(component.__owl__.vnode!, fiber.vnode!);
} else {
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
component.__owl__.currentFiber = null;
}
// insert into the DOM (mount case)
let inDOM = false;
if (this.inserter) {
this.inserter(this.component.el!);
if (this.target) {
switch (this.position) {
case "first-child":
this.target.prepend(this.component.el!);
break;
case "last-child":
this.target.appendChild(this.component.el!);
break;
}
inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
}
@@ -227,12 +259,12 @@ export class Fiber {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.inserter) {
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else if (this.inserter ? inDOM : true) {
} else if (this.target ? inDOM : true) {
component.__callMounted();
}
}
+12 -1
View File
@@ -34,7 +34,7 @@ export class Scheduler {
this.isRunning = false;
}
addFiber(fiber): Promise<void> {
addFiber(fiber: Fiber): Promise<void> {
// if the fiber was remapped into a larger rendering fiber, it may not be a
// root fiber. But we only want to register root fibers
fiber = fiber.root;
@@ -57,6 +57,17 @@ export class Scheduler {
});
}
rejectFiber(fiber: Fiber, reason: string) {
fiber = fiber.root;
const index = this.tasks.findIndex(t => t.fiber === fiber);
if (index >= 0) {
const [task] = this.tasks.splice(index, 1);
fiber.cancel();
fiber.error = new Error(reason);
task.callback();
}
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.