Skip to content

Commit

Permalink
[IMP] component: allow top level widgets
Browse files Browse the repository at this point in the history
closes #166
  • Loading branch information
ged-odoo committed Jul 12, 2019
1 parent d9cbd23 commit 591508e
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 11 deletions.
14 changes: 11 additions & 3 deletions src/qweb_core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
41 changes: 33 additions & 8 deletions src/qweb_extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand All @@ -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();

Expand Down
111 changes: 111 additions & 0 deletions tests/__snapshots__/component.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}"
`;
101 changes: 101 additions & 0 deletions tests/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3819,3 +3819,104 @@ describe("component error handling (catchError)", () => {
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
});
});

describe("top level sub widgets", () => {
test("basic use", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<Child p="1"/>
</t>
<span t-name="Child">child<t t-esc="props.p"/></span>
</templates>`);
class Child extends Widget {}
class Parent extends Widget {
components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>child1</span>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});


test("sub widget is interactive", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<Child p="1"/>
</t>
<span t-name="Child"><button t-on-click="inc">click</button>child<t t-esc="state.val"/></span>
</templates>`);
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("<span><button>click</button>child1</span>");
const button = fixture.querySelector('button')!;
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("<span><button>click</button>child2</span>");
});

test("can select a sub widget ", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<t t-if="env.flag"><Child /></t>
<t t-if="!env.flag"><OtherChild /></t>
</t>
<span t-name="Child">CHILD 1</span>
<div t-name="OtherChild">CHILD 2</div>
</templates>`);
class Child extends Widget {}
class OtherChild extends Widget {}
class Parent extends Widget {
components = { Child, OtherChild };
}
(<any>env).flag = true;
let parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>CHILD 1</span>");
parent.destroy();
(<any>env).flag = false;
parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");

expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});

test("can select a sub widget, part 2", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<t t-if="state.flag"><Child /></t>
<t t-if="!state.flag"><OtherChild /></t>
</t>
<span t-name="Child">CHILD 1</span>
<div t-name="OtherChild">CHILD 2</div>
</templates>`);
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("<span>CHILD 1</span>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");

});

});

0 comments on commit 591508e

Please sign in to comment.