mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[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:
@@ -3021,7 +3021,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
static template = xml`
|
||||
<div class="widget-subkey">
|
||||
<t t-esc="props.key"/>__<t t-esc="props.subKey"/>
|
||||
</div>`
|
||||
</div>`;
|
||||
}
|
||||
class Child extends Component {
|
||||
static components = { Custom };
|
||||
@@ -3029,7 +3029,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
<t t-component="Custom"
|
||||
t-key="props.subKey"
|
||||
key="props.key"
|
||||
subKey="props.subKey"/>`
|
||||
subKey="props.subKey"/>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
@@ -3045,24 +3045,24 @@ describe("random stuff/miscellaneous", () => {
|
||||
}
|
||||
const parent = new Parent(null);
|
||||
await parent.mount(fixture);
|
||||
expect(fixture.textContent!.trim()).toBe('1__1');
|
||||
expect(fixture.textContent!.trim()).toBe("1__1");
|
||||
|
||||
// First step: change the Custom's instance
|
||||
Object.assign(parent.childProps, {
|
||||
subKey: 2,
|
||||
subKey: 2,
|
||||
});
|
||||
parent.render();
|
||||
await nextTick();
|
||||
expect(fixture.textContent!.trim()).toBe('1__2');
|
||||
expect(fixture.textContent!.trim()).toBe("1__2");
|
||||
|
||||
// Second step, change both Child's and Custom's instance
|
||||
Object.assign(parent.childProps, {
|
||||
key: 2,
|
||||
subKey: 3,
|
||||
key: 2,
|
||||
subKey: 3,
|
||||
});
|
||||
parent.render();
|
||||
await nextTick();
|
||||
expect(fixture.textContent!.trim()).toBe('2__3');
|
||||
expect(fixture.textContent!.trim()).toBe("2__3");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -509,4 +509,103 @@ describe("unmounting and remounting", () => {
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Cannot mount a destroyed component");
|
||||
});
|
||||
|
||||
test("destroying a sub-component cleans itself from parent's vnode", async () => {
|
||||
class C1 extends Component {
|
||||
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
|
||||
}
|
||||
class P extends Component {
|
||||
static components = { C1 };
|
||||
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/></div></div>`;
|
||||
state = {
|
||||
a: "first",
|
||||
};
|
||||
}
|
||||
const parent = new P();
|
||||
await parent.mount(fixture);
|
||||
expect(fixture.textContent).toBe("first");
|
||||
parent.unmount();
|
||||
parent.state.a = "";
|
||||
parent.mount(fixture);
|
||||
parent.state.a = "fixed";
|
||||
await parent.render();
|
||||
expect(fixture.textContent).toBe("fixed");
|
||||
});
|
||||
|
||||
test("destroying a sub-component cleans itself from parent's vnode, part 2", async () => {
|
||||
class C1 extends Component {
|
||||
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
|
||||
}
|
||||
class P extends Component {
|
||||
static components = { C1 };
|
||||
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/>some text</div></div>`;
|
||||
state = {
|
||||
a: "first",
|
||||
};
|
||||
}
|
||||
const parent = new P();
|
||||
await parent.mount(fixture);
|
||||
expect(fixture.textContent).toBe("firstsome text");
|
||||
parent.unmount();
|
||||
parent.state.a = "";
|
||||
parent.mount(fixture);
|
||||
parent.state.a = "fixed";
|
||||
await parent.render();
|
||||
expect(fixture.textContent).toBe("fixedsome text");
|
||||
});
|
||||
|
||||
test("destroying a sub-component cleans itself from parent's vnode, part 3", async () => {
|
||||
class C1 extends Component {
|
||||
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
|
||||
}
|
||||
|
||||
class C2 extends Component {
|
||||
static template = xml`<C1 a="props.a"/>`;
|
||||
static components = { C1 };
|
||||
}
|
||||
|
||||
class P extends Component {
|
||||
static components = { C2 };
|
||||
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/></div></div>`;
|
||||
state = {
|
||||
a: "first",
|
||||
};
|
||||
}
|
||||
const parent = new P();
|
||||
await parent.mount(fixture);
|
||||
expect(fixture.textContent).toBe("first");
|
||||
parent.unmount();
|
||||
parent.state.a = "";
|
||||
parent.mount(fixture);
|
||||
parent.state.a = "fixed";
|
||||
await parent.render();
|
||||
expect(fixture.textContent).toBe("fixed");
|
||||
});
|
||||
|
||||
test("destroying a sub-component cleans itself from parent's vnode, part 4", async () => {
|
||||
class C1 extends Component {
|
||||
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
|
||||
}
|
||||
|
||||
class C2 extends Component {
|
||||
static template = xml`<C1 a="props.a"/>`;
|
||||
static components = { C1 };
|
||||
}
|
||||
class P extends Component {
|
||||
static components = { C2 };
|
||||
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/>some text</div></div>`;
|
||||
state = {
|
||||
a: "first",
|
||||
};
|
||||
}
|
||||
const parent = new P();
|
||||
await parent.mount(fixture);
|
||||
expect(fixture.textContent).toBe("firstsome text");
|
||||
parent.unmount();
|
||||
parent.state.a = "";
|
||||
parent.mount(fixture);
|
||||
parent.state.a = "fixed";
|
||||
await parent.render();
|
||||
expect(fixture.textContent).toBe("fixedsome text");
|
||||
});
|
||||
});
|
||||
|
||||
+4
-4
@@ -1107,9 +1107,9 @@ describe("html to vdom", function () {
|
||||
});
|
||||
|
||||
test("svg", function () {
|
||||
const nodeList = htmlToVDOM(`<svg></svg>`);
|
||||
expect(nodeList).toHaveLength(1);
|
||||
elm = patch(vnode0, nodeList[0]).elm;
|
||||
expect(elm).toBeInstanceOf(SVGSVGElement);
|
||||
const nodeList = htmlToVDOM(`<svg></svg>`);
|
||||
expect(nodeList).toHaveLength(1);
|
||||
elm = patch(vnode0, nodeList[0]).elm;
|
||||
expect(elm).toBeInstanceOf(SVGSVGElement);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user