diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 2b9c0e7a..b3aa587d 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -1115,8 +1115,11 @@ export class CodeGenerator { let slotStr: string[] = []; for (let slotName in ast.slots) { const slotAst = ast.slots[slotName]; - const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on); - const params = [`__render: ${name}, __ctx: ${ctxStr}`]; + const params = []; + if (slotAst.content) { + const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on); + params.push(`__render: ${name}, __ctx: ${ctxStr}`); + } const scope = ast.slots[slotName].scope; if (scope) { params.push(`__scope: "${scope}"`); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 16547c01..2e315835 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -118,7 +118,7 @@ export interface ASTTCall { } interface SlotDefinition { - content: AST; + content: AST | null; scope: string | null; on: EventHandlers | null; attrs: Attrs | null; @@ -749,26 +749,24 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { slotNode.removeAttribute("t-set-slot"); slotNode.remove(); const slotAst = parseNode(slotNode, ctx); - if (slotAst) { - let on: SlotDefinition["on"] = null; - let attrs: Attrs | null = null; - let scope: string | null = null; - for (let attributeName of slotNode.getAttributeNames()) { - const value = slotNode.getAttribute(attributeName)!; - if (attributeName === "t-slot-scope") { - scope = value; - continue; - } else if (attributeName.startsWith("t-on-")) { - on = on || {}; - on[attributeName.slice(5)] = value; - } else { - attrs = attrs || {}; - attrs[attributeName] = value; - } + let on: SlotDefinition["on"] = null; + let attrs: Attrs | null = null; + let scope: string | null = null; + for (let attributeName of slotNode.getAttributeNames()) { + const value = slotNode.getAttribute(attributeName)!; + if (attributeName === "t-slot-scope") { + scope = value; + continue; + } else if (attributeName.startsWith("t-on-")) { + on = on || {}; + on[attributeName.slice(5)] = value; + } else { + attrs = attrs || {}; + attrs[attributeName] = value; } - slots = slots || {}; - slots[name] = { content: slotAst, on, attrs, scope }; } + slots = slots || {}; + slots[name] = { content: slotAst, on, attrs, scope }; } // default slot diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts index c8959148..3402f89f 100644 --- a/tests/compiler/parser.test.ts +++ b/tests/compiler/parser.test.ts @@ -1370,6 +1370,25 @@ describe("qweb parser", () => { }); }); + test("a component with an empty named slot", async () => { + expect(parse(``)).toEqual({ + dynamicProps: null, + isDynamic: false, + name: "MyComponent", + on: null, + props: null, + slots: { + mySlot: { + attrs: null, + content: null, + on: null, + scope: null, + }, + }, + type: 11, + }); + }); + test("a component with a named slot", async () => { expect(parse(`foo`)).toEqual({ type: ASTType.TComponent, diff --git a/tests/components/__snapshots__/slots.test.ts.snap b/tests/components/__snapshots__/slots.test.ts.snap index e98f2726..c58a6727 100644 --- a/tests/components/__snapshots__/slots.test.ts.snap +++ b/tests/components/__snapshots__/slots.test.ts.snap @@ -1444,6 +1444,71 @@ exports[`slots simple default slot, variation 2`] = ` }" `; +exports[`slots simple named and empty slot -- 2 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { capture, markRaw } = helpers; + + return function template(ctx, node, key = \\"\\") { + const ctx1 = capture(ctx); + return component(\`Child\`, {slots: markRaw({'myEmptySlot': {myProp: 'myProp text'}})}, key + \`__1\`, node, ctx); + } +}" +`; + +exports[`slots simple named and empty slot -- 2 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot } = helpers; + + let block1 = createBlock(\`\`); + + function defaultContent1(ctx, node, key = \\"\\") { + return text(\`default empty\`); + } + + return function template(ctx, node, key = \\"\\") { + const b3 = callSlot(ctx, node, key, 'myEmptySlot', false, null, defaultContent1); + return block1([], [b3]); + } +}" +`; + +exports[`slots simple named and empty slot 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { capture, markRaw } = helpers; + + function slot1(ctx, node, key = \\"\\") { + return text(\`some text\`); + } + + return function template(ctx, node, key = \\"\\") { + const ctx1 = capture(ctx); + return component(\`Child\`, {slots: markRaw({'myEmptySlot': {}, 'default': {__render: slot1, __ctx: ctx1}})}, key + \`__1\`, node, ctx); + } +}" +`; + +exports[`slots simple named and empty slot 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + const b2 = callSlot(ctx, node, key, 'default', false, null); + const b3 = callSlot(ctx, node, key, 'myEmptySlot', false, null); + return block1([], [b2, b3]); + } +}" +`; + exports[`slots simple slot with slot scope 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/slots.test.ts b/tests/components/slots.test.ts index 729b2fda..9cbf325b 100644 --- a/tests/components/slots.test.ts +++ b/tests/components/slots.test.ts @@ -74,6 +74,43 @@ describe("slots", () => { expect(fixture.innerHTML).toBe("other text"); }); + test("simple named and empty slot", async () => { + class Child extends Component { + static template = xml``; + + setup() { + expect(this.props.slots["myEmptySlot"]).toBeTruthy(); + } + } + + class Parent extends Component { + static template = xml`some text`; + static components = { Child }; + } + await mount(Parent, fixture); + + expect(fixture.innerHTML).toBe("some text"); + }); + + test("simple named and empty slot -- 2", async () => { + class Child extends Component { + static template = xml`default empty`; + + setup() { + expect(this.props.slots["myEmptySlot"]).toBeTruthy(); + expect(this.props.slots["myEmptySlot"].myProp).toBe("myProp text"); + } + } + + class Parent extends Component { + static template = xml``; + static components = { Child }; + } + await mount(Parent, fixture); + + expect(fixture.innerHTML).toBe("default empty"); + }); + test("default slot with slot scope: shorthand syntax", async () => { let child: any; class Child extends Component {