From 56086242bbb60ce91f83ac3c425e457660639dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 7 Mar 2022 13:16:47 +0100 Subject: [PATCH] [IMP] compiler: add support for t-on- on t-slots and t-set-slots --- src/compiler/code_generator.ts | 59 +++++++++----- src/compiler/parser.ts | 29 ++++++- tests/compiler/parser.test.ts | 31 ++++++++ .../__snapshots__/t_on.test.ts.snap | 76 +++++++++++++++++++ tests/components/t_on.test.ts | 49 ++++++++++++ 5 files changed, 221 insertions(+), 23 deletions(-) diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 60ea6292..403bcf0b 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -157,9 +157,11 @@ class CodeTarget { // maps ref name to [id, expr] refInfo: { [name: string]: [string, string] } = {}; shouldProtectScope: boolean = false; + on?: { [key: string]: string }; - constructor(name: string) { + constructor(name: string, on?: { [key: string]: string }) { this.name = name; + this.on = on; } addLine(line: string, idx?: number) { @@ -303,14 +305,18 @@ export class CodeGenerator { return code; } - compileInNewTarget(prefix: string, ast: AST, ctx: Context): string { + compileInNewTarget( + prefix: string, + ast: AST, + ctx: Context, + on?: { [key: string]: string } + ): string { const name = this.generateId(prefix); const initialTarget = this.target; - const target = new CodeTarget(name); + const target = new CodeTarget(name, on); this.targets.push(target); this.target = target; - const subCtx: Context = createContext(ctx); - this.compileAST(ast, subCtx); + this.compileAST(ast, createContext(ctx)); this.target = initialTarget; return name; } @@ -372,6 +378,9 @@ export class CodeGenerator { } if (block.isRoot && !ctx.preventRoot) { + if (this.target.on) { + blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on); + } this.addLine(`return ${blockExpr};`); } else { this.define(block.varName, blockExpr); @@ -1110,8 +1119,8 @@ export class CodeGenerator { } let slotStr: string[] = []; for (let slotName in ast.slots) { - const slotAst = ast.slots[slotName].content; - const name = this.compileInNewTarget("slot", slotAst, ctx); + const slotAst = ast.slots[slotName]; + const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on || undefined); const params = [`__render: ${name}, __ctx: ${ctxStr}`]; const scope = ast.slots[slotName].scope; if (scope) { @@ -1183,24 +1192,29 @@ export class CodeGenerator { // event handling if (ast.on) { - this.helpers.add("createCatcher"); - let name = this.generateId("catcher"); - let spec: any = {}; - let handlers: any[] = []; - for (let ev in ast.on) { - let handlerId = this.generateId("hdlr"); - let idx = handlers.push(handlerId) - 1; - spec[ev] = idx; - const handler = this.generateHandlerCode(ev, ast.on[ev]); - this.define(handlerId, handler); - } - blockExpr = `${name}(${blockExpr}, [${handlers.join(",")}])`; - this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` }); + blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on); } + block = this.createBlock(block, "multi", ctx); this.insertBlock(blockExpr, block, ctx); } + wrapWithEventCatcher(expr: string, on: { [key: string]: string }): string { + this.helpers.add("createCatcher"); + let name = this.generateId("catcher"); + let spec: any = {}; + let handlers: any[] = []; + for (let ev in on) { + let handlerId = this.generateId("hdlr"); + let idx = handlers.push(handlerId) - 1; + spec[ev] = idx; + const handler = this.generateHandlerCode(ev, on[ev]); + this.define(handlerId, handler); + } + this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` }); + return `${name}(${expr}, [${handlers.join(",")}])`; + } + compileTSlot(ast: ASTSlot, ctx: Context) { this.helpers.add("callSlot"); let { block } = ctx; @@ -1227,6 +1241,11 @@ export class CodeGenerator { blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`; } } + // event handling + if (ast.on) { + blockString = this.wrapWithEventCatcher(blockString, ast.on); + } + if (block) { this.insertAnchor(block); } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 86159959..55c1b2d3 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -114,6 +114,13 @@ export interface ASTTCall { body: AST[] | null; } +interface SlotDefinition { + content: AST; + attrs?: { [key: string]: string }; + scope?: string; + on?: null | { [key: string]: string }; +} + export interface ASTComponent { type: ASTType.TComponent; name: string; @@ -121,13 +128,14 @@ export interface ASTComponent { dynamicProps: string | null; on: null | { [key: string]: string }; props: { [name: string]: string }; - slots: { [name: string]: { content: AST; attrs?: { [key: string]: string }; scope?: string } }; + slots: { [name: string]: SlotDefinition }; } export interface ASTSlot { type: ASTType.TSlot; name: string; attrs: { [key: string]: string }; + on: null | { [key: string]: string }; defaultContent: AST | null; } @@ -736,18 +744,26 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { const slotAst = parseNode(slotNode, ctx); if (slotAst) { const slotInfo: any = { content: slotAst }; + let on: SlotDefinition["on"] = null; const attrs: { [key: string]: string } = {}; for (let attributeName of slotNode.getAttributeNames()) { const value = slotNode.getAttribute(attributeName)!; if (attributeName === "t-slot-scope") { slotInfo.scope = value; continue; + } else if (attributeName.startsWith("t-on-")) { + on = on || {}; + on[attributeName.slice(5)] = value; + } else { + attrs[attributeName] = value; } - attrs[attributeName] = value; } if (Object.keys(attrs).length) { slotInfo.attrs = attrs; } + if (on) { + slotInfo.on = on; + } slots[name] = slotInfo; } } @@ -775,14 +791,21 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null { const name = node.getAttribute("t-slot")!; node.removeAttribute("t-slot"); const attrs: { [key: string]: string } = {}; + let on: ASTComponent["on"] = null; for (let attributeName of node.getAttributeNames()) { const value = node.getAttribute(attributeName)!; - attrs[attributeName] = value; + if (attributeName.startsWith("t-on-")) { + on = on || {}; + on[attributeName.slice(5)] = value; + } else { + attrs[attributeName] = value; + } } return { type: ASTType.TSlot, name, attrs, + on, defaultContent: parseChildNodes(node, ctx), }; } diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts index 13f8bbe0..ee0bad50 100644 --- a/tests/compiler/parser.test.ts +++ b/tests/compiler/parser.test.ts @@ -1378,6 +1378,25 @@ describe("qweb parser", () => { }); }); + test("a component with a named slot with t-on", async () => { + expect( + parse(`foo`) + ).toEqual({ + type: ASTType.TComponent, + name: "MyComponent", + isDynamic: false, + dynamicProps: null, + props: {}, + on: null, + slots: { + name: { + content: { type: ASTType.Text, value: "foo" }, + on: { click: "doStuff" }, + }, + }, + }); + }); + test("a component with a named slot with div tag", async () => { expect(() => parse(`
foo
`) @@ -1550,6 +1569,7 @@ describe("qweb parser", () => { type: ASTType.TSlot, name: "default", attrs: {}, + on: null, defaultContent: null, }); }); @@ -1559,10 +1579,21 @@ describe("qweb parser", () => { type: ASTType.TSlot, name: "header", attrs: {}, + on: null, defaultContent: { type: ASTType.Text, value: "default content" }, }); }); + test("t-slot with t-on-", async () => { + expect(parse(``)).toEqual({ + type: ASTType.TSlot, + name: "default", + attrs: {}, + on: { "click.prevent": "doSomething" }, + defaultContent: null, + }); + }); + // --------------------------------------------------------------------------- // t-debug // --------------------------------------------------------------------------- diff --git a/tests/components/__snapshots__/t_on.test.ts.snap b/tests/components/__snapshots__/t_on.test.ts.snap index f359030c..ee11bc78 100644 --- a/tests/components/__snapshots__/t_on.test.ts.snap +++ b/tests/components/__snapshots__/t_on.test.ts.snap @@ -210,3 +210,79 @@ exports[`t-on t-on on destroyed components 2`] = ` } }" `; + +exports[`t-on t-on on t-set-slots 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { capture, createCatcher, markRaw } = helpers; + const catcher1 = createCatcher({\\"click\\":0}); + + let block6 = createBlock(\`

something

\`); + let block7 = createBlock(\`

paragraph

\`); + + function slot1(ctx, node, key = \\"\\") { + const b6 = block6(); + const b7 = block7(); + const hdlr1 = [()=>this.state.count++, ctx]; + return catcher1(multi([b6, b7]), [hdlr1]); + } + + return function template(ctx, node, key = \\"\\") { + const b2 = text(\` [\`); + const b3 = text(ctx['state'].count); + const b4 = text(\`] \`); + const ctx1 = capture(ctx); + const b8 = component(\`Child\`, {slots: markRaw({'myslot': {__render: slot1, __ctx: ctx1}})}, key + \`__1\`, node, ctx); + return multi([b2, b3, b4, b8]); + } +}" +`; + +exports[`t-on t-on on t-set-slots 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot } = helpers; + + return function template(ctx, node, key = \\"\\") { + return callSlot(ctx, node, key, 'myslot', false, {}); + } +}" +`; + +exports[`t-on t-on on t-slots 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { markRaw } = helpers; + + let block1 = createBlock(\`

something

\`); + + function slot1(ctx, node, key = \\"\\") { + return block1(); + } + + return function template(ctx, node, key = \\"\\") { + return component(\`Child\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__1\`, node, ctx); + } +}" +`; + +exports[`t-on t-on on t-slots 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot, createCatcher } = helpers; + const catcher1 = createCatcher({\\"click\\":0}); + + return function template(ctx, node, key = \\"\\") { + const b2 = text(\` [\`); + const b3 = text(ctx['state'].count); + const b4 = text(\`] \`); + const hdlr1 = [()=>this.state.count++, ctx]; + const b5 = catcher1(callSlot(ctx, node, key, 'default', false, {}), [hdlr1]); + return multi([b2, b3, b4, b5]); + } +}" +`; diff --git a/tests/components/t_on.test.ts b/tests/components/t_on.test.ts index 613f3dd8..2dc4e61f 100644 --- a/tests/components/t_on.test.ts +++ b/tests/components/t_on.test.ts @@ -196,4 +196,53 @@ describe("t-on", () => { await nextTick(); expect(fixture.innerHTML).toBe("

"); }); + + test("t-on on t-slots", async () => { + class Child extends Component { + static template = xml` + [] + `; + + state = useState({ count: 0 }); + } + + class Parent extends Component { + static template = xml` + +

something

+
`; + static components = { Child }; + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe(" [0]

something

"); + fixture.querySelector("p")!.click(); + await nextTick(); + expect(fixture.innerHTML).toBe(" [1]

something

"); + }); + + test("t-on on t-set-slots", async () => { + class Child extends Component { + static template = xml``; + } + + class Parent extends Component { + static template = xml` + [] + + +

something

+

paragraph

+
+
`; + static components = { Child }; + state = useState({ count: 0 }); + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe(" [0]

something

paragraph

"); + fixture.querySelector("p")!.click(); + await nextTick(); + expect(fixture.innerHTML).toBe(" [1]

something

paragraph

"); + }); });