[FIX] component: subtle issue with unmounted children

Owl has to manage a lot of interesting situations.  One of them is when
a rendering is initiated, which creates a sub component, but then
another rendering starts, which invalidate the previous one, and will
create another sub component.  Since the first sub component was not
ever in the DOM, we cannot rely on the vdom patching process to remove
it, so we have to do it manually.

Sadly, this is actually a very tricky situation, since there are other
subtle situations where the code that remove an unmounted widget could
be executed, in particular when the parent component is unmounted, then
remounted, then modified to trigger yet another rendering.

In this commit, we handle this case more carefully by making sure that
the destroyed subcomponent properly configures its pvnode so the patch
process happens as expected.

joint work with the framework team, and in particular LPE for his work on
finding a testcase!

closes #724, #731
This commit is contained in:
Géry Debongnie
2020-09-14 21:17:52 +02:00
committed by aab-odoo
parent 81af21a025
commit 8d25bddda4
6 changed files with 137 additions and 23 deletions
+22 -3
View File
@@ -656,9 +656,28 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) {
let child = __owl__.children[childKey];
if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) {
child.destroy();
const child = __owl__.children[childKey];
const childOwl = child.__owl__;
if (!childOwl.isMounted && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
// if some of its parent have been unmounted, the child could actually
// still be attached to its parent, and this may be important if we
// want to remount the parent, because the vdom need to match the
// actual DOM
child.__destroy(childOwl.parent);
if (childOwl.pvnode) {
// we remove the key here to make sure that the patching algorithm
// is able to make the difference between this pvnode and an eventual
// other instance of the same component
delete childOwl.pvnode.key;
// Since the component has been unmounted, we do not want to actually
// call a remove hook. This is pretty important, since the t-component
// directive actually disabled it, so the vdom algorithm will just
// not remove the child elm if we don't remove the hook.
delete childOwl.pvnode.data!.hook!.remove;
}
}
}
if (!vnode) {
+2 -6
View File
@@ -275,16 +275,12 @@ QWeb.addDirective({
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) {
ctx.addLine(
`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`
);
ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
} else {
// this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`);
ctx.addLine(
`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`
);
ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
ctx.addLine(`result = result[0]`);
}
+2 -2
View File
@@ -216,8 +216,8 @@ export class QWeb extends EventBus {
// id, and a (global) mapping from an id to the compiled function. This is
// necessary to ensure that global templates can be called with more than one
// QWeb instance.
subTemplates: {[key: string]: number} = {};
static subTemplates: {[id: number]: Function} = {};
subTemplates: { [key: string]: number } = {};
static subTemplates: { [id: number]: Function } = {};
isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];