diff --git a/doc/reference/templates.md b/doc/reference/templates.md index f0af6699..1cd613ca 100644 --- a/doc/reference/templates.md +++ b/doc/reference/templates.md @@ -540,6 +540,15 @@ This can be used to define variables scoped to a sub template: ``` +Note: by default, the rendering context for a sub template is simply the current +rendering context (so, the current component). However, it may be useful to be +able to specify a specific object as context. This can be done by using the +`t-call-context` directive: + +```xml + +``` + ### Dynamic sub templates The `t-call` directive can also be used to dynamically call a sub template, diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index e0714226..6bb513ff 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -140,6 +140,7 @@ interface Context { tKeyExpr: string | null; nameSpace?: string; tModelSelectedExpr?: string; + ctxVar?: string; } function createContext(parentCtx: Context, params?: Partial) { @@ -967,16 +968,21 @@ export class CodeGenerator { compileTCall(ast: ASTTCall, ctx: Context) { let { block, forceNewBlock } = ctx; + let ctxVar = ctx.ctxVar || "ctx"; + if (ast.context) { + ctxVar = generateId("ctx"); + this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`); + } if (ast.body) { - this.addLine(`ctx = Object.create(ctx);`); - this.addLine(`ctx[isBoundary] = 1;`); + this.addLine(`${ctxVar} = Object.create(${ctxVar});`); + this.addLine(`${ctxVar}[isBoundary] = 1;`); this.helpers.add("isBoundary"); const nextId = BlockDescription.nextBlockId; - const subCtx: Context = createContext(ctx, { preventRoot: true }); + const subCtx: Context = createContext(ctx, { preventRoot: true, ctxVar }); this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); if (nextId !== BlockDescription.nextBlockId) { this.helpers.add("zero"); - this.addLine(`ctx[zero] = b${nextId};`); + this.addLine(`${ctxVar}[zero] = b${nextId};`); } } const isDynamic = INTERP_REGEXP.test(ast.name); @@ -994,7 +1000,7 @@ export class CodeGenerator { } this.define(templateVar, subTemplate); block = this.createBlock(block, "multi", ctx); - this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block!, { + this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block!, { ...ctx, forceNewBlock: !block, }); @@ -1002,13 +1008,13 @@ export class CodeGenerator { const id = generateId(`callTemplate_`); this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` }); block = this.createBlock(block, "multi", ctx); - this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block!, { + this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block!, { ...ctx, forceNewBlock: !block, }); } if (ast.body && !ctx.isLast) { - this.addLine(`ctx = ctx.__proto__;`); + this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`); } } @@ -1046,7 +1052,7 @@ export class CodeGenerator { value = expr; } this.helpers.add("setContextValue"); - this.addLine(`setContextValue(ctx, "${ast.name}", ${value});`); + this.addLine(`setContextValue(${ctx.ctxVar || "ctx"}, "${ast.name}", ${value});`); } } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 2e315835..540e4a70 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -115,6 +115,7 @@ export interface ASTTCall { type: ASTType.TCall; name: string; body: AST[] | null; + context: string | null; } interface SlotDefinition { @@ -557,11 +558,13 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null { return null; } const subTemplate = node.getAttribute("t-call")!; - + const context = node.getAttribute("t-call-context"); node.removeAttribute("t-call"); + node.removeAttribute("t-call-context"); + if (node.tagName !== "t") { const ast = parseNode(node, ctx); - const tcall: AST = { type: ASTType.TCall, name: subTemplate, body: null }; + const tcall: AST = { type: ASTType.TCall, name: subTemplate, body: null, context }; if (ast && ast.type === ASTType.DomNode) { ast.content = [tcall]; return ast; @@ -579,6 +582,7 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null { type: ASTType.TCall, name: subTemplate, body: body.length ? body : null, + context, }; } diff --git a/tests/compiler/__snapshots__/t_call.test.ts.snap b/tests/compiler/__snapshots__/t_call.test.ts.snap index a370e1f9..148914da 100644 --- a/tests/compiler/__snapshots__/t_call.test.ts.snap +++ b/tests/compiler/__snapshots__/t_call.test.ts.snap @@ -617,6 +617,36 @@ exports[`t-call (template calling) t-call allowed on a non t node 2`] = ` }" `; +exports[`t-call (template calling) t-call on a div with t-call-context 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const callTemplate_1 = app.getTemplate(\`sub\`); + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let ctx1 = ctx['obj']; + const b2 = callTemplate_1.call(this, ctx1, node, key + \`__1\`); + return block1([], [b2]); + } +}" +`; + +exports[`t-call (template calling) t-call on a div with t-call-context 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['value']; + return block1([txt1]); + } +}" +`; + exports[`t-call (template calling) t-call with body content as root of a template 1`] = ` "function anonymous(app, bdom, helpers ) { @@ -890,6 +920,67 @@ exports[`t-call (template calling) t-call, conditional and t-set in t-call body }" `; +exports[`t-call (template calling) t-call-context 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const callTemplate_1 = app.getTemplate(\`sub\`); + + return function template(ctx, node, key = \\"\\") { + let ctx1 = ctx['obj']; + return callTemplate_1.call(this, ctx1, node, key + \`__1\`); + } +}" +`; + +exports[`t-call (template calling) t-call-context 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['value']; + return block1([txt1]); + } +}" +`; + +exports[`t-call (template calling) t-call-context and value in body 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { isBoundary, withDefault, setContextValue } = helpers; + const callTemplate_1 = app.getTemplate(\`sub\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + let ctx1 = ctx['obj']; + ctx1 = Object.create(ctx1); + ctx1[isBoundary] = 1; + setContextValue(ctx1, \\"value2\\", ctx['aaron']); + return callTemplate_1.call(this, ctx1, node, key + \`__1\`); + } +}" +`; + +exports[`t-call (template calling) t-call-context and value in body 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['value1']; + let txt2 = ctx['value2']; + return block1([txt1, txt2]); + } +}" +`; + exports[`t-call (template calling) t-esc inside t-call, with t-set outside 1`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts index 3402f89f..52178251 100644 --- a/tests/compiler/parser.test.ts +++ b/tests/compiler/parser.test.ts @@ -1033,6 +1033,7 @@ describe("qweb parser", () => { type: ASTType.TCall, name: "blap", body: null, + context: null, }, memo: "", hasNoFirst: false, @@ -1070,6 +1071,7 @@ describe("qweb parser", () => { type: ASTType.TCall, name: "blabla", body: null, + context: null, }); }); @@ -1077,10 +1079,20 @@ describe("qweb parser", () => { expect(parse(`ok`)).toEqual({ type: ASTType.TCall, name: "sub", + context: null, body: [{ type: ASTType.Text, value: "ok" }], }); }); + test("t-call expression with t-call-context", async () => { + expect(parse(``)).toEqual({ + type: ASTType.TCall, + name: "blabla", + body: null, + context: "someContext", + }); + }); + test("t-call on a div node", async () => { expect(parse(`
`)).toEqual({ type: ASTType.DomNode, @@ -1096,6 +1108,7 @@ describe("qweb parser", () => { type: ASTType.TCall, name: "blabla", body: null, + context: null, }, ], }); @@ -1111,6 +1124,7 @@ describe("qweb parser", () => { type: ASTType.TCall, name: "blabla", body: null, + context: null, }, }); }); @@ -1547,7 +1561,7 @@ describe("qweb parser", () => { on: null, slots: { default: { - content: { body: null, name: "subTemplate", type: ASTType.TCall }, + content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null }, attrs: null, scope: null, on: null, diff --git a/tests/compiler/t_call.test.ts b/tests/compiler/t_call.test.ts index e6bc371c..13b8c967 100644 --- a/tests/compiler/t_call.test.ts +++ b/tests/compiler/t_call.test.ts @@ -445,4 +445,38 @@ describe("t-call (template calling)", () => { const expected2 = "
quux
"; expect(context.renderToString("main", { template: "bar", val: "quux" })).toBe(expected2); }); + + test("t-call-context", () => { + const context = new TestContext(); + context.addTemplate("sub", ``); + context.addTemplate("main", ``); + + expect(context.renderToString("main", { obj: { value: 123 } })).toBe("123"); + }); + + test("t-call on a div with t-call-context", () => { + const context = new TestContext(); + context.addTemplate("sub", ``); + context.addTemplate("main", `
`); + + expect(context.renderToString("main", { obj: { value: 123 } })).toBe( + "
123
" + ); + }); + + test("t-call-context and value in body", () => { + const context = new TestContext(); + context.addTemplate("sub", ``); + context.addTemplate( + "main", + ` + + + ` + ); + + expect(context.renderToString("main", { obj: { value1: 123 }, aaron: "lucas" })).toBe( + "123lucas" + ); + }); }); diff --git a/tests/components/__snapshots__/t_call.test.ts.snap b/tests/components/__snapshots__/t_call.test.ts.snap index 1a3f8a0a..aa4521d2 100644 --- a/tests/components/__snapshots__/t_call.test.ts.snap +++ b/tests/components/__snapshots__/t_call.test.ts.snap @@ -402,3 +402,70 @@ exports[`t-call t-call in t-foreach and children component 3`] = ` } }" `; + +exports[`t-call t-call with t-call-context and subcomponent 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const callTemplate_1 = app.getTemplate(\`someTemplate\`); + + return function template(ctx, node, key = \\"\\") { + let ctx1 = ctx['subctx']; + return callTemplate_1.call(this, ctx1, node, key + \`__1\`); + } +}" +`; + +exports[`t-call t-call with t-call-context and subcomponent 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(\`Child\`, true, false, false, false); + const comp2 = app.createComponent(\`Child\`, true, false, false, false); + + return function template(ctx, node, key = \\"\\") { + const b2 = comp1({name: ctx['aab']}, key + \`__1\`, node, this, null); + const b3 = comp2({name: ctx['lpe']}, key + \`__2\`, node, this, null); + return multi([b2, b3]); + } +}" +`; + +exports[`t-call t-call with t-call-context and subcomponent 3`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + const b2 = text(\`child\`); + const b3 = text(ctx['props'].name); + return multi([b2, b3]); + } +}" +`; + +exports[`t-call t-call with t-call-context, simple use 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const callTemplate_1 = app.getTemplate(\`someTemplate\`); + + return function template(ctx, node, key = \\"\\") { + let ctx1 = ctx['subctx']; + return callTemplate_1.call(this, ctx1, node, key + \`__1\`); + } +}" +`; + +exports[`t-call t-call with t-call-context, simple use 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + const b2 = text(ctx['aab']); + const b3 = text(ctx['lpe']); + return multi([b2, b3]); + } +}" +`; diff --git a/tests/components/t_call.test.ts b/tests/components/t_call.test.ts index 5f74ea66..4d5bd476 100644 --- a/tests/components/t_call.test.ts +++ b/tests/components/t_call.test.ts @@ -244,4 +244,47 @@ describe("t-call", () => { } expect(clickCount).toBe(2); }); + + test("t-call with t-call-context, simple use", async () => { + class Root extends Component { + static template = xml` + `; + + subctx = { aab: "aaron", lpe: "lucas" }; + } + + await mount(Root, fixture, { + templates: ` + + + `, + }); + expect(fixture.innerHTML).toBe("aaronlucas"); + }); + + test("t-call with t-call-context and subcomponent", async () => { + class Child extends Component { + static template = xml`child`; + } + + class Root extends Component { + static template = xml` + `; + + static components = { Child }; + + subctx = { aab: "aaron", lpe: "lucas" }; + } + + await mount(Root, fixture, { + templates: ` + + + + + + `, + }); + expect(fixture.innerHTML).toBe("childaaronchildlucas"); + }); });