diff --git a/web/static/src/ts/core/Widget.ts b/web/static/src/ts/core/Widget.ts index e0bd77ff..6ec8d913 100644 --- a/web/static/src/ts/core/Widget.ts +++ b/web/static/src/ts/core/Widget.ts @@ -96,8 +96,12 @@ export class Widget { destroy() { if (!this.__widget__.isDestroyed) { + if (this.__widget__.isMounted) { + this.willUnmount(); + } if (this.el) { this.el.remove(); + this.__widget__.isMounted = false; delete this.__widget__.vnode; } if (this.__widget__.parent) { diff --git a/web/static/src/ts/core/qweb_vdom.ts b/web/static/src/ts/core/qweb_vdom.ts index 73589475..48f1face 100644 --- a/web/static/src/ts/core/qweb_vdom.ts +++ b/web/static/src/ts/core/qweb_vdom.ts @@ -704,7 +704,7 @@ const widgetDirective: Directive = { ctx.addLine( `let def${defID} = _${widgetID}._start().then(() => _${widgetID}._render()).then(vnode=>{c${ ctx.parentNode - }[_${dummyID}_index]=vnode;vnode.data.hook = {create(_,vn){_${widgetID}._mount(vn)}}});` + }[_${dummyID}_index]=vnode;vnode.data.hook = {create(_,vn){_${widgetID}._mount(vn)},remove(){_${widgetID}.destroy()}}});` ); ctx.addLine(`extra.promises.push(def${defID});`); diff --git a/web/static/src/ts/widgets/clock.ts b/web/static/src/ts/widgets/clock.ts index e0f81573..0496777a 100644 --- a/web/static/src/ts/widgets/clock.ts +++ b/web/static/src/ts/widgets/clock.ts @@ -6,6 +6,8 @@ const template = `
`; export class Clock extends Widget { name = "clock"; template = template; + interval: any | undefined; + state = { currentTime: "" }; @@ -15,9 +17,12 @@ export class Clock extends Widget { } mounted() { - setInterval(this.updateTime.bind(this), 500); + this.interval = setInterval(this.updateTime.bind(this), 500); } + willUnmount() { + clearInterval(this.interval); + } updateTime() { this.updateState({ currentTime: new Date().toLocaleTimeString() diff --git a/web/static/tests/core/widget.test.ts b/web/static/tests/core/widget.test.ts index 29870f67..fcc45299 100644 --- a/web/static/tests/core/widget.test.ts +++ b/web/static/tests/core/widget.test.ts @@ -269,6 +269,49 @@ describe("lifecycle hooks", () => { const widget = new ParentWidget(env); await widget.mount(target); }); + + test("widgets are unmounted and destroyed if no longer in DOM", async () => { + let steps: string[] = []; + class ParentWidget extends Widget { + name = "a"; + state = { ok: true }; + template = ` +
+ +
`; + widgets = { child: ChildWidget }; + } + + class ChildWidget extends Widget { + constructor(parent) { + super(parent); + steps.push("init"); + } + async willStart() { + steps.push("willstart"); + } + mounted() { + steps.push("mounted"); + } + willUnmount() { + steps.push("willunmount"); + } + destroyed() { + steps.push("destroyed"); + } + } + const widget = new ParentWidget(env); + await widget.mount(fixture); + expect(steps).toEqual(["init", "willstart", "mounted"]); + await widget.updateState({ ok: false }); + expect(steps).toEqual([ + "init", + "willstart", + "mounted", + "willunmount", + "destroyed" + ]); + }); }); describe("destroy method", () => { @@ -278,6 +321,8 @@ describe("destroy method", () => { expect(document.contains(widget.el)).toBe(true); widget.destroy(); expect(document.contains(widget.el)).toBe(false); + expect(widget.__widget__.isMounted).toBe(false); + expect(widget.__widget__.isDestroyed).toBe(true); }); test("destroying a widget twice only call destroyed once", async () => {