[FIX] component: hooks mounted/willUnmount called in correct order

The issue was with parent/child relationship.  It is not intuitive to
me, but the mounted hook should be called first on the children, and
then on the parent.  willUnmount hooks is the opposite.

closes #63
This commit is contained in:
Géry Debongnie
2019-04-26 16:20:50 +02:00
parent b44f274e37
commit 005b727328
2 changed files with 80 additions and 75 deletions
+52 -45
View File
@@ -219,28 +219,38 @@ export class Component<
target.appendChild(this.el!); target.appendChild(this.el!);
if (document.body.contains(target)) { if (document.body.contains(target)) {
this._visitSubTree(w => { this._callMounted();
if (!w.__owl__.isMounted && this.el!.contains(w.el)) { }
w.__owl__.isMounted = true; }
w.mounted();
return true; _callMounted() {
} const children = this.__owl__.children;
return false; for (let id in children) {
}); const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp._callMounted();
}
}
this.__owl__.isMounted = true;
this.mounted();
}
_callWillUnmount() {
this.willUnmount();
this.__owl__.isMounted = false;
const children = this.__owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp._callWillUnmount();
}
} }
} }
unmount() { unmount() {
if (this.el) { if (this.__owl__.isMounted) {
this._visitSubTree(w => { this._callWillUnmount();
if (w.__owl__.isMounted) { this.el!.remove();
w.willUnmount();
w.__owl__.isMounted = false;
return true;
}
return false;
});
this.el.remove();
} }
} }
@@ -260,27 +270,34 @@ export class Component<
destroy() { destroy() {
if (!this.__owl__.isDestroyed) { if (!this.__owl__.isDestroyed) {
for (let id in this.__owl__.children) { const el = this.el;
this.__owl__.children[id].destroy(); this._destroy();
if (el) {
el.remove();
} }
if (this.__owl__.isMounted) {
this.willUnmount();
}
if (this.el) {
this.el.remove();
this.__owl__.isMounted = false;
delete this.__owl__.vnode;
}
if (this.__owl__.parent) {
let id = this.__owl__.id;
delete this.__owl__.parent.__owl__.children[id];
this.__owl__.parent = null;
}
this.clear();
this.__owl__.isDestroyed = true;
} }
} }
_destroy() {
const isMounted = this.__owl__.isMounted;
if (isMounted) {
this.willUnmount();
this.__owl__.isMounted = false;
}
const children = Object.values(this.__owl__.children);
for (let child of children) {
child._destroy();
}
if (this.__owl__.parent) {
let id = this.__owl__.id;
delete this.__owl__.parent.__owl__.children[id];
this.__owl__.parent = null;
}
this.clear();
this.__owl__.isDestroyed = true;
delete this.__owl__.vnode;
}
shouldUpdate(nextProps: Props): boolean { shouldUpdate(nextProps: Props): boolean {
return true; return true;
} }
@@ -412,16 +429,6 @@ export class Component<
} }
} }
_visitSubTree(callback: (w: Component<T, any, any>) => boolean) {
const shouldVisitChildren = callback(this);
if (shouldVisitChildren) {
const children = this.__owl__.children;
for (let id in children) {
children[id]._visitSubTree(callback);
}
}
}
_observeState() { _observeState() {
if (this.state) { if (this.state) {
this.__owl__.observer = new Observer(); this.__owl__.observer = new Observer();
+28 -30
View File
@@ -196,32 +196,29 @@ describe("lifecycle hooks", () => {
}); });
test("mounted hook is called on subwidgets, in proper order", async () => { test("mounted hook is called on subwidgets, in proper order", async () => {
expect.assertions(4); const steps: any[] = [];
let parentMounted = false;
let childMounted = false;
class ParentWidget extends Widget { class ParentWidget extends Widget {
inlineTemplate = `<div><t t-widget="child"/></div>`; inlineTemplate = `<div><t t-widget="child"/></div>`;
widgets = { child: ChildWidget }; widgets = { child: ChildWidget };
mounted() { mounted() {
expect(childMounted).toBe(false); steps.push("parent:mounted");
parentMounted = true;
} }
} }
class ChildWidget extends Widget { class ChildWidget extends Widget {
mounted() { mounted() {
expect(document.body.contains(this.el)).toBe(true); expect(document.body.contains(this.el)).toBe(true);
expect(parentMounted).toBe(true); steps.push("child:mounted");
childMounted = true;
} }
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(childMounted).toBe(true); expect(steps).toEqual(["child:mounted", "parent:mounted"]);
}); });
test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => { test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => {
expect.assertions(3); const steps: string[] = [];
let hookCounter = 0;
// the t-else part in the template is important. This is // the t-else part in the template is important. This is
// necessary to have a situation that could confuse the vdom // necessary to have a situation that could confuse the vdom
// patching algorithm // patching algorithm
@@ -240,19 +237,18 @@ describe("lifecycle hooks", () => {
} }
class ChildWidget extends Widget { class ChildWidget extends Widget {
async willStart() { async willStart() {
hookCounter++; steps.push("child:willStart");
} }
mounted() { mounted() {
expect(hookCounter).toBe(1); steps.push("child:mounted");
hookCounter++;
} }
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(hookCounter).toBe(0); // sub widget not created yet expect(steps).toEqual([]);
widget.state.ok = true; widget.state.ok = true;
await nextTick(); await nextTick();
expect(hookCounter).toBe(2); expect(steps).toEqual(["child:willStart", "child:mounted"]);
}); });
test("mounted hook is correctly called on subwidgets created in mounted hook", async done => { test("mounted hook is correctly called on subwidgets created in mounted hook", async done => {
@@ -397,10 +393,10 @@ describe("lifecycle hooks", () => {
"p willstart", "p willstart",
"c init", "c init",
"c willstart", "c willstart",
"p mounted",
"c mounted", "c mounted",
"c willunmount", "p mounted",
"p willunmount" "p willunmount",
"c willunmount"
]); ]);
}); });
@@ -545,10 +541,10 @@ describe("lifecycle hooks", () => {
state = { n: 1 }; state = { n: 1 };
willPatch() { willPatch() {
steps.push("parent:willPatch"); steps.push("parent:willPatch");
return 'leffe'; return "leffe";
} }
patched(snapshot) { patched(snapshot) {
expect(snapshot).toBe('leffe'); expect(snapshot).toBe("leffe");
steps.push("parent:patched"); steps.push("parent:patched");
} }
} }
@@ -576,7 +572,6 @@ describe("lifecycle hooks", () => {
]); ]);
}); });
test("willPatch/patched hook with t-keepalive", async () => { test("willPatch/patched hook with t-keepalive", async () => {
// we make sure here that willPatch/patched is only called if widget is in // we make sure here that willPatch/patched is only called if widget is in
// dom, mounted // dom, mounted
@@ -606,15 +601,18 @@ describe("lifecycle hooks", () => {
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(steps).toEqual(['child:mounted']); expect(steps).toEqual(["child:mounted"]);
widget.state.flag = false; widget.state.flag = false;
await nextTick(); await nextTick();
expect(steps).toEqual(['child:mounted', 'child:willUnmount']); expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
widget.state.flag = true; widget.state.flag = true;
await nextTick(); await nextTick();
expect(steps).toEqual(['child:mounted', 'child:willUnmount', 'child:mounted']); expect(steps).toEqual([
"child:mounted",
"child:willUnmount",
"child:mounted"
]);
}); });
}); });
describe("destroy method", () => { describe("destroy method", () => {
@@ -675,7 +673,7 @@ describe("destroy method", () => {
expect(widget.__owl__.isStarted).toBe(false); expect(widget.__owl__.isStarted).toBe(false);
expect(widget.__owl__.isMounted).toBe(false); expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true); expect(widget.__owl__.isDestroyed).toBe(true);
expect(widget.__owl__.vnode).toBe(null); expect(widget.__owl__.vnode).toBe(undefined);
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(isRendered).toBe(false); expect(isRendered).toBe(false);
}); });
@@ -707,7 +705,7 @@ describe("composition", () => {
<t t-foreach="state.list" t-ref="'child'" t-widget="Widget"/> <t t-foreach="state.list" t-ref="'child'" t-widget="Widget"/>
</div>`; </div>`;
widgets = { Widget }; widgets = { Widget };
state = {list: <any>[]}; state = { list: <any>[] };
willPatch() { willPatch() {
expect(this.refs.child).toBeUndefined(); expect(this.refs.child).toBeUndefined();
} }
@@ -719,7 +717,7 @@ describe("composition", () => {
const parent = new ParentWidget(env); const parent = new ParentWidget(env);
await parent.mount(fixture); await parent.mount(fixture);
parent.state.list.push(1); parent.state.list.push(1);
await nextTick() await nextTick();
}); });
test("t-refs are bound at proper timing (2)", async () => { test("t-refs are bound at proper timing (2)", async () => {
@@ -763,9 +761,9 @@ describe("composition", () => {
const parent = new ParentWidget(env); const parent = new ParentWidget(env);
await parent.mount(fixture); await parent.mount(fixture);
parent.state.child2 = true; parent.state.child2 = true;
await nextTick() await nextTick();
parent.state.child1 = false; parent.state.child1 = false;
await nextTick() await nextTick();
}); });
test("modifying a sub widget", async () => { test("modifying a sub widget", async () => {