diff --git a/README.md b/README.md index 526352d5..1a8a6797 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,16 @@ We have 3 main folders and 3 main files: else, and make sure they are properly connected. Then, it make sure that the root widget is mounted to its desired location. +## New QWeb directives + +1. t-on-\*: allow binding event handlers on any kind of event +2. t-widget: instantiate a widget at this location. It has sub directives: + a. t-props: to pass properties to the widget constructor/updateProps methods + b. t-key: useful to keep track of which widget is which, when created in a + list + c. t-keep-alive: makes sure the widget is not destroyed when removed from + the DOM (note that it will still be destroyed when parent is destroyed) + ## Random Notes - Change of behaviour: The compilation of a template should have a unique root @@ -187,3 +197,5 @@ We have 3 main folders and 3 main files: nextanimationframe - when t-widget/t-on directives are compiled, the widget/eval context is actually available. Should we use that info to determine if bound methods exists on the widget? + +- allow t-if t-widget and t-else t-widget on same t tag diff --git a/web/static/src/ts/core/Widget.ts b/web/static/src/ts/core/Widget.ts index 2c4fa6d9..9c370b5c 100644 --- a/web/static/src/ts/core/Widget.ts +++ b/web/static/src/ts/core/Widget.ts @@ -112,11 +112,27 @@ export class Widget { if (!w.__widget__.isMounted && this.el!.contains(w.el)) { w.__widget__.isMounted = true; w.mounted(); + return true; } + return false; }); } } + detach() { + if (this.el) { + this.visitSubTree(w => { + if (w.__widget__.isMounted) { + w.willUnmount(); + w.__widget__.isMounted = false; + return true; + } + return false; + }); + this.el.remove(); + } + } + destroy() { if (!this.__widget__.isDestroyed) { for (let id in this.__widget__.children) { @@ -221,11 +237,13 @@ export class Widget { return this.__widget__.vnode; } - private visitSubTree(callback: (w: Widget) => void) { - callback(this); - const children = this.__widget__.children; - for (let id in children) { - children[id].visitSubTree(callback); + private visitSubTree(callback: (w: Widget) => boolean) { + const shouldVisitChildren = callback(this); + if (shouldVisitChildren) { + const children = this.__widget__.children; + for (let id in children) { + children[id].visitSubTree(callback); + } } } } diff --git a/web/static/src/ts/core/qweb_vdom.ts b/web/static/src/ts/core/qweb_vdom.ts index f029a3e8..ab0cc440 100644 --- a/web/static/src/ts/core/qweb_vdom.ts +++ b/web/static/src/ts/core/qweb_vdom.ts @@ -703,8 +703,10 @@ const widgetDirective: Directive = { name: "widget", priority: 100, atNodeEncounter({ ctx, value, node, qweb }): boolean { + ctx.addLine("//WIDGET"); ctx.rootContext.shouldDefineOwner = true; let props = node.getAttribute("t-props"); + let keepAlive = node.getAttribute("t-keep-alive") ? true : false; let key = node.getAttribute("t-key"); if (key) { key = qweb._formatExpression(key); @@ -748,9 +750,11 @@ const widgetDirective: Directive = { ctx.addLine(`if (w${widgetID}) {`); ctx.indent(); ctx.addLine( - `def${defID} = w${widgetID}.updateProps(${props}).then(()=>{let vnode=h(w${widgetID}.__widget__.vnode.sel, {key: ${templateID}});c${ + `def${defID} = w${widgetID}.updateProps(${props}).then(()=>{let vnode=h(w${widgetID}.__widget__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;c${ ctx.parentNode - }[_${dummyID}_index]=vnode;vnode.data.hook = {remove(){w${widgetID}.destroy()}}});` + }[_${dummyID}_index]=vnode;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;},remove(){w${widgetID}.${ + keepAlive ? "detach" : "destroy" + }()}}});` ); ctx.dedent(); ctx.addLine("} else {"); @@ -765,7 +769,9 @@ const widgetDirective: Directive = { ctx.addLine( `def${defID} = _${widgetID}._start().then(() => _${widgetID}._render()).then(vnode=>{let pvnode=h(vnode.sel, {key: ${templateID}});c${ ctx.parentNode - }[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=_${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){_${widgetID}.destroy()}}});` + }[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=_${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){_${widgetID}.${ + keepAlive ? "detach" : "destroy" + }()}}});` ); let ref = node.getAttribute("t-ref"); diff --git a/web/static/tests/core/widget.test.ts b/web/static/tests/core/widget.test.ts index 3877eb00..0bcedb65 100644 --- a/web/static/tests/core/widget.test.ts +++ b/web/static/tests/core/widget.test.ts @@ -584,6 +584,54 @@ describe("composition", () => { ); }); + test("sub widgets with t-keep-alive are not destroyed if no longer in dom", async () => { + class ParentWidget extends Widget { + name = "a"; + state = { ok: true }; + template = ` +
`; + widgets = { counter: Counter }; + } + const widget = new ParentWidget(env); + await widget.mount(fixture); + const button = fixture.getElementsByTagName("button")[0]; + await button.click(); + await nextTick(); + expect(fixture.innerHTML).toBe( + "
1
" + ); + await widget.updateState({ ok: false }); + expect(fixture.innerHTML).toBe("
"); + await widget.updateState({ ok: true }); + expect(fixture.innerHTML).toBe( + "
1
" + ); + }); + + test("sub widgets dom state with t-keep-alive is preserved", async () => { + class ParentWidget extends Widget { + name = "a"; + state = { ok: true }; + template = ` +
`; + widgets = { InputWidget }; + } + class InputWidget extends Widget { + template = ``; + } + const widget = new ParentWidget(env); + await widget.mount(fixture); + const input = fixture.getElementsByTagName("input")[0]; + input.value = "test"; + await widget.updateState({ ok: false }); + expect(fixture.innerHTML).toBe("
"); + await widget.updateState({ ok: true }); + expect(fixture.innerHTML).toBe("
"); + const input2 = fixture.getElementsByTagName("input")[0]; + expect(input).toBe(input2); + expect(input2.value).toBe("test"); + }); + test("sub widgets rendered in a loop", async () => { class ChildWidget extends Widget { name = "c";