From 591508e7690d65a2af0f414c9e9a766c9da7e058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 11 Jul 2019 16:19:59 +0200 Subject: [PATCH] [IMP] component: allow top level widgets closes #166 --- src/qweb_core.ts | 14 ++- src/qweb_extensions.ts | 41 ++++++-- tests/__snapshots__/component.test.ts.snap | 111 +++++++++++++++++++++ tests/component.test.ts | 101 +++++++++++++++++++ 4 files changed, 256 insertions(+), 11 deletions(-) diff --git a/src/qweb_core.ts b/src/qweb_core.ts index 430f4153e..d9448c30e 100644 --- a/src/qweb_core.ts +++ b/src/qweb_core.ts @@ -329,10 +329,14 @@ export class QWeb extends EventBus { this._compileNode(elem, ctx); if (!parentContext) { - if (!ctx.rootNode) { - throw new Error(`A template should have one root node (${ctx.templateName})`); + if (ctx.shouldDefineResult) { + ctx.addLine(`return result;`); + } else { + if (!ctx.rootNode) { + throw new Error(`A template should have one root node (${ctx.templateName})`); + } + ctx.addLine(`return vn${ctx.rootNode};`); } - ctx.addLine(`return vn${ctx.rootNode};`); } let code = ctx.generateCode(); @@ -689,6 +693,7 @@ export class Context { shouldDefineParent: boolean = false; shouldDefineQWeb: boolean = false; shouldDefineUtils: boolean = false; + shouldDefineResult: boolean = false; shouldProtectContext: boolean = false; shouldTrackScope: boolean = false; inLoop: boolean = false; @@ -723,6 +728,9 @@ export class Context { if (this.shouldProtectContext) { this.code.unshift(" context = Object.create(context);"); } + if (this.shouldDefineResult) { + this.code.unshift(" let result;"); + } if (this.shouldDefineOwner) { // this is necessary to prevent some directives (t-forach for ex) to // pollute the rendering context by adding some keys in it. diff --git a/src/qweb_extensions.ts b/src/qweb_extensions.ts index c11ed5797..aecb5d14e 100644 --- a/src/qweb_extensions.ts +++ b/src/qweb_extensions.ts @@ -190,6 +190,19 @@ const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, { self: "if (e.target !== vn.elm) {return}" }); +UTILS.defineProxy = function defineProxy(target, source) { + for (let k in source) { + Object.defineProperty(target, k, { + get() { + return source[k]; + }, + set(val) { + source[k] = val; + } + }); + } +}; + /** * The t-component directive is certainly a complicated and hard to maintain piece * of code. To help you, fellow developer, if you have to maintain it, I offer @@ -519,14 +532,26 @@ QWeb.addDirective({ ctx.addLine( `let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;` ); - ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); + if (ctx.parentNode) { + ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); + } + let shouldProxy = false; if (async) { ctx.addLine(`const patchQueue${componentID} = [];`); ctx.addLine( `c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);` ); } else { - ctx.addLine(`c${ctx.parentNode}.push(null);`); + if (ctx.parentNode) { + ctx.addLine(`c${ctx.parentNode}.push(null);`); + } else { + let id = ctx.generateID(); + ctx.rootContext.rootNode = id; + shouldProxy = true; + ctx.rootContext.shouldDefineResult = true; + ctx.addLine(`let vn${id} = {};`); + ctx.addLine(`result = vn${id};`); + } } ctx.addLine(`let props${componentID} = {${propStr}};`); ctx.addIf( @@ -600,10 +625,12 @@ QWeb.addDirective({ } ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`); // hack: specify empty remove hook to prevent the node from being removed from the DOM + let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`; + if (shouldProxy) { + registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`; + } ctx.addLine( - `def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${ - ctx.parentNode - }[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});` + `def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});` ); ctx.addElse(); @@ -623,9 +650,7 @@ QWeb.addDirective({ ctx.addLine( `def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${ tattStyle ? `w${componentID}.el.style=${tattStyle};` : "" - }let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${ - ctx.parentNode - }[_${dummyID}_index]=pvnode;});` + }let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});` ); ctx.closeIf(); diff --git a/tests/__snapshots__/component.test.ts.snap b/tests/__snapshots__/component.test.ts.snap index c302e6aa2..40cf1eb02 100644 --- a/tests/__snapshots__/component.test.ts.snap +++ b/tests/__snapshots__/component.test.ts.snap @@ -1443,3 +1443,114 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = ` return vn1; }" `; + +exports[`top level sub widgets basic use 1`] = ` +"function anonymous(context,extra +) { + let utils = this.utils; + let QWeb = this.constructor; + let parent = context; + let owner = context; + let result; + var h = this.utils.h; + //COMPONENT + let def2; + let w3 = 3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[3]] : false; + let vn4 = {}; + result = vn4; + let props3 = {p:1}; + if (w3 && w3.__owl__.renderPromise && !w3.__owl__.vnode) { + if (utils.shallowEqual(props3, w3.__owl__.renderProps)) { + def2 = w3.__owl__.renderPromise; + } else { + w3.destroy(); + w3 = false; + } + } + if (!w3) { + let componentKey3 = \`Child\`; + let W3 = context.components && context.components[componentKey3] || QWeb.components[componentKey3]; + if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} + w3 = new W3(parent, props3); + parent.__owl__.cmap[3] = w3.__owl__.id; + def2 = w3.__prepare(); + def2 = def2.then(vnode=>{let pvnode=h(vnode.sel, {key: 3, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn4, pvnode);w3.__owl__.pvnode = pvnode;}); + } else { + def2 = def2 || w3.__updateProps(props3, extra.forceUpdate, extra.patchQueue); + def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn4, pvnode);}); + } + extra.promises.push(def2); + return result; +}" +`; + +exports[`top level sub widgets can select a sub widget 1`] = ` +"function anonymous(context,extra +) { + let utils = this.utils; + let QWeb = this.constructor; + let parent = context; + let owner = context; + let result; + var h = this.utils.h; + if (context['env'].flag) { + //COMPONENT + let def2; + let w3 = 3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[3]] : false; + let vn4 = {}; + result = vn4; + let props3 = {}; + if (w3 && w3.__owl__.renderPromise && !w3.__owl__.vnode) { + if (utils.shallowEqual(props3, w3.__owl__.renderProps)) { + def2 = w3.__owl__.renderPromise; + } else { + w3.destroy(); + w3 = false; + } + } + if (!w3) { + let componentKey3 = \`Child\`; + let W3 = context.components && context.components[componentKey3] || QWeb.components[componentKey3]; + if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} + w3 = new W3(parent, props3); + parent.__owl__.cmap[3] = w3.__owl__.id; + def2 = w3.__prepare(); + def2 = def2.then(vnode=>{let pvnode=h(vnode.sel, {key: 3, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn4, pvnode);w3.__owl__.pvnode = pvnode;}); + } else { + def2 = def2 || w3.__updateProps(props3, extra.forceUpdate, extra.patchQueue); + def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn4, pvnode);}); + } + extra.promises.push(def2); + } + if (!context['env'].flag) { + //COMPONENT + let def6; + let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false; + let vn8 = {}; + result = vn8; + let props7 = {}; + if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) { + if (utils.shallowEqual(props7, w7.__owl__.renderProps)) { + def6 = w7.__owl__.renderPromise; + } else { + w7.destroy(); + w7 = false; + } + } + if (!w7) { + let componentKey7 = \`OtherChild\`; + let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7]; + if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')} + w7 = new W7(parent, props7); + parent.__owl__.cmap[7] = w7.__owl__.id; + def6 = w7.__prepare(); + def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});utils.defineProxy(vn8, pvnode);w7.__owl__.pvnode = pvnode;}); + } else { + def6 = def6 || w7.__updateProps(props7, extra.forceUpdate, extra.patchQueue); + def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;utils.defineProxy(vn8, pvnode);}); + } + extra.promises.push(def6); + } + return result; +}" +`; diff --git a/tests/component.test.ts b/tests/component.test.ts index c3feee48e..271e0a2c5 100644 --- a/tests/component.test.ts +++ b/tests/component.test.ts @@ -3819,3 +3819,104 @@ describe("component error handling (catchError)", () => { expect(fixture.innerHTML).toBe("
Error handled
"); }); }); + +describe("top level sub widgets", () => { + test("basic use", async () => { + env.qweb.addTemplates(` + + + + + child + `); + class Child extends Widget {} + class Parent extends Widget { + components = { Child }; + } + const parent = new Parent(env); + await parent.mount(fixture); + expect(fixture.innerHTML).toBe("child1"); + expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + }); + + + test("sub widget is interactive", async () => { + env.qweb.addTemplates(` + + + + + child + `); + class Child extends Widget { + state = {val: 1}; + inc() { + this.state.val++; + } + } + class Parent extends Widget { + components = { Child }; + } + const parent = new Parent(env); + await parent.mount(fixture); + expect(fixture.innerHTML).toBe("child1"); + const button = fixture.querySelector('button')!; + button.click(); + await nextTick(); + expect(fixture.innerHTML).toBe("child2"); + }); + + test("can select a sub widget ", async () => { + env.qweb.addTemplates(` + + + + + + CHILD 1 +
CHILD 2
+
`); + class Child extends Widget {} + class OtherChild extends Widget {} + class Parent extends Widget { + components = { Child, OtherChild }; + } + (env).flag = true; + let parent = new Parent(env); + await parent.mount(fixture); + expect(fixture.innerHTML).toBe("CHILD 1"); + parent.destroy(); + (env).flag = false; + parent = new Parent(env); + await parent.mount(fixture); + expect(fixture.innerHTML).toBe("
CHILD 2
"); + + expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + }); + + test("can select a sub widget, part 2", async () => { + env.qweb.addTemplates(` + + + + + + CHILD 1 +
CHILD 2
+
`); + class Child extends Widget {} + class OtherChild extends Widget {} + class Parent extends Widget { + state = {flag: true} + components = { Child, OtherChild }; + } + let parent = new Parent(env); + await parent.mount(fixture); + expect(fixture.innerHTML).toBe("CHILD 1"); + parent.state.flag = false; + await nextTick(); + expect(fixture.innerHTML).toBe("
CHILD 2
"); + + }); + +});