From 56cfc6403da5717b9294c07cfbc1721e65a842da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 20 Feb 2023 17:04:49 +0100 Subject: [PATCH] wip --- src/compiler/code_generator.ts | 85 ++++++---- src/compiler/parser.ts | 154 ++++++++++-------- .../__snapshots__/slots.test.ts.snap | 41 +++++ tests/components/slots.test.ts | 29 ++++ 4 files changed, 208 insertions(+), 101 deletions(-) diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 3b9a0e72..9f6592df 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -15,6 +15,7 @@ import { ASTLog, ASTMulti, ASTSlot, + ASTSlotDefinition, ASTTCall, ASTTCallBlock, ASTTEsc, @@ -137,12 +138,14 @@ interface Context { index: number | string; forceNewBlock: boolean; preventRoot?: boolean; + ignoreRoot?: boolean; isLast?: boolean; translate: boolean; tKeyExpr: string | null; nameSpace?: string; tModelSelectedExpr?: string; ctxVar?: string; + slotVar?: string; } function createContext(parentCtx: Context, params?: Partial): Context { @@ -155,6 +158,7 @@ function createContext(parentCtx: Context, params?: Partial): Context { tKeyExpr: null, nameSpace: parentCtx.nameSpace, tModelSelectedExpr: parentCtx.tModelSelectedExpr, + slotVar: parentCtx.slotVar }, params ); @@ -384,6 +388,9 @@ export class CodeGenerator { blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`; } + if (ctx.ignoreRoot) { + return; + } if (block.isRoot && !ctx.preventRoot) { if (this.target.on) { blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on); @@ -465,6 +472,8 @@ export class CodeGenerator { return this.compileLog(ast, ctx); case ASTType.TSlot: return this.compileTSlot(ast, ctx); + case ASTType.TSetSlot: + return this.compileTSetSlot(ast, ctx); case ASTType.TTranslation: return this.compileTTranslation(ast, ctx); case ASTType.TPortal: @@ -1144,52 +1153,31 @@ export class CodeGenerator { const props: string[] = ast.props ? this.formatPropObject(ast.props) : []; // slots - let slotDef: string = ""; - if (ast.slots) { - let ctxStr = "ctx"; - if (this.target.loopLevel || !this.hasSafeContext) { - ctxStr = generateId("ctx"); - this.helpers.add("capture"); - this.define(ctxStr, `capture(ctx)`); - } - let slotStr: string[] = []; - for (let slotName in ast.slots) { - const slotAst = ast.slots[slotName]; - const params = []; - if (slotAst.content) { - const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on); - params.push(`__render: ${name}.bind(this), __ctx: ${ctxStr}`); - } - const scope = ast.slots[slotName].scope; - if (scope) { - params.push(`__scope: "${scope}"`); - } - if (ast.slots[slotName].attrs) { - params.push(...this.formatPropObject(ast.slots[slotName].attrs!)); - } - const slotInfo = `{${params.join(", ")}}`; - slotStr.push(`'${slotName}': ${slotInfo}`); - } - slotDef = `{${slotStr.join(", ")}}`; + let slotVar: string = ""; + if (ast.body) { + slotVar = generateId("slot"); + const subCtx = createContext(ctx, { slotVar, ignoreRoot: true }); + this.target.addLine(`let ${slotVar} = {};`); + this.compileAST(ast.body, subCtx); } - if (slotDef && !(ast.dynamicProps || hasSlotsProp)) { + if (slotVar && !(ast.dynamicProps || hasSlotsProp)) { this.helpers.add("markRaw"); - props.push(`slots: markRaw(${slotDef})`); + props.push(`slots: markRaw(${slotVar})`); } let propString = this.getPropString(props, ast.dynamicProps); let propVar: string; - if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) { + if ((slotVar && (ast.dynamicProps || hasSlotsProp)) || this.dev) { propVar = generateId("props"); this.define(propVar!, propString); propString = propVar!; } - if (slotDef && (ast.dynamicProps || hasSlotsProp)) { + if (slotVar && (ast.dynamicProps || hasSlotsProp)) { this.helpers.add("markRaw"); - this.addLine(`${propVar!}.slots = markRaw(Object.assign(${slotDef}, ${propVar!}.slots))`); + this.addLine(`${propVar!}.slots = markRaw(Object.assign(${slotVar}, ${propVar!}.slots))`); } // cmap key @@ -1220,7 +1208,7 @@ export class CodeGenerator { id, expr: `app.createComponent(${ ast.isDynamic ? null : expr - }, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, ${ + }, ${!ast.isDynamic}, ${!!ast.body}, ${!!ast.dynamicProps}, ${ !ast.props && !ast.dynamicProps })`, }); @@ -1263,6 +1251,37 @@ export class CodeGenerator { return `${name}(${expr}, [${handlers.join(",")}])`; } + compileTSetSlot(ast: ASTSlotDefinition, ctx: Context): string { + let ctxStr = "ctx"; + if (this.target.loopLevel || !this.hasSafeContext) { + ctxStr = generateId("ctx"); + this.helpers.add("capture"); + this.define(ctxStr, `capture(ctx)`); + } + // let slotStr: string[] = []; + // for (let slotName in ast.slots) { + // const slotAst = ast.slots[slotName]; + const params = []; + if (ast.content) { + const name = this.compileInNewTarget("slotFn", ast.content, ctx, ast.on); + params.push(`__render: ${name}.bind(this), __ctx: ${ctxStr}`); + } + // ast.scope + // const scope = ast.slots[slotName].scope; + if (ast.scope) { + params.push(`__scope: "${ast.scope}"`); + } + if (ast.attrs) { + params.push(...this.formatPropObject(ast.attrs!)); + } + const slotInfo = `{${params.join(", ")}}`; + this.target.addLine(`${ctx.slotVar}['${ast.name}'] = ${slotInfo};`) + // slotStr.push(`'${slotName}': ${slotInfo}`); + // } + // slotDef = `{${slotStr.join(", ")}}`; + return "what should go here? anyone knows?"; + } + compileTSlot(ast: ASTSlot, ctx: Context): string { this.helpers.add("callSlot"); let { block } = ctx; diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 0bf572d9..27bb4c65 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -23,6 +23,7 @@ export const enum ASTType { TDebug, TLog, TSlot, + TSetSlot, TCallBlock, TTranslation, TPortal, @@ -120,7 +121,9 @@ export interface ASTTCall { context: string | null; } -interface SlotDefinition { +export interface ASTSlotDefinition { + type: ASTType.TSetSlot; + name: string; content: AST | null; scope: string | null; on: EventHandlers | null; @@ -134,7 +137,8 @@ export interface ASTComponent { dynamicProps: string | null; on: EventHandlers | null; props: { [name: string]: string } | null; - slots: { [name: string]: SlotDefinition } | null; + body: AST | null; + // slots: { [name: string]: ASTSlotDefinition } | null; } export interface ASTSlot { @@ -186,6 +190,7 @@ export type AST = | ASTTKey | ASTComponent | ASTSlot + | ASTSlotDefinition | ASTTCallBlock | ASTLog | ASTDebug @@ -238,6 +243,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null { parseTKey(node, ctx) || parseTTranslation(node, ctx) || parseTSlot(node, ctx) || + parseTSetSlot(node, ctx) || parseTOutNode(node, ctx) || parseComponent(node, ctx) || parseDOMNode(node, ctx) || @@ -573,12 +579,12 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null { ast.content = [tcall]; return ast; } - if (ast && ast.type === ASTType.TComponent) { - return { - ...ast, - slots: { default: { content: tcall, scope: null, on: null, attrs: null } }, - }; - } + // if (ast && ast.type === ASTType.TComponent) { + // return { + // ...ast, + // slots: { default: { content: tcall, scope: null, on: null, attrs: null } }, + // }; + // } } const body = parseChildren(node, ctx); @@ -706,8 +712,8 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { const dynamicProps = node.getAttribute("t-props"); node.removeAttribute("t-props"); - const defaultSlotScope = node.getAttribute("t-slot-scope"); - node.removeAttribute("t-slot-scope"); + // const defaultSlotScope = node.getAttribute("t-slot-scope"); + // node.removeAttribute("t-slot-scope"); let on: ASTComponent["on"] = null; let props: ASTComponent["props"] = null; @@ -727,67 +733,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { } } - let slots: ASTComponent["slots"] | null = null; + let body: ASTComponent["body"] = null; + + // let slots: ASTComponent["slots"] | null = null; if (node.hasChildNodes()) { - const clone = node.cloneNode(true); + body = parseChildNodes(node, ctx); - // named slots - const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]")); - for (let slotNode of slotNodes) { - if (slotNode.tagName !== "t") { - throw new OwlError( - `Directive 't-set-slot' can only be used on nodes (used on a <${slotNode.tagName}>)` - ); - } - const name = slotNode.getAttribute("t-set-slot")!; - - // check if this is defined in a sub component (in which case it should - // be ignored) - let el = slotNode.parentElement!; - let isInSubComponent = false; - while (el !== clone) { - if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) { - isInSubComponent = true; - break; - } - el = el.parentElement!; - } - if (isInSubComponent) { - continue; - } - - slotNode.removeAttribute("t-set-slot"); - slotNode.remove(); - const slotAst = parseNode(slotNode, ctx); - 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 }; - } - - // default slot - const defaultContent = parseChildNodes(clone, ctx); - slots = slots || {}; - // t-set-slot="default" has priority over content - if (defaultContent && !slots.default) { - slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope }; - } } - return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots, on }; + return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, body, on }; } // ----------------------------------------------------------------------------- @@ -821,6 +774,71 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null { }; } +function parseTSetSlot(node: Element, ctx: ParsingContext): AST | null { + if (!node.hasAttribute("t-set-slot")) { + return null; + } + // const t = el.ownerDocument.createElement("t"); + + // const clone = node.cloneNode(true); + + // // named slots + // const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]")); + // for (let slotNode of slotNodes) { + if (node.tagName !== "t") { + throw new OwlError( + `Directive 't-set-slot' can only be used on nodes (used on a <${node.tagName}>)` + ); + } + const name = node.getAttribute("t-set-slot")!; + + // // check if this is defined in a sub component (in which case it should + // // be ignored) + // let el = slotNode.parentElement!; + // let isInSubComponent = false; + // while (el !== clone) { + // if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) { + // isInSubComponent = true; + // break; + // } + // el = el.parentElement!; + // } + // if (isInSubComponent) { + // continue; + // } + + node.removeAttribute("t-set-slot"); + node.remove(); + const slotAst = parseNode(node, ctx); + let on: ASTSlotDefinition["on"] = null; + let attrs: Attrs | null = null; + let scope: string | null = null; + for (let attributeName of node.getAttributeNames()) { + const value = node.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 || {}; + return { type: ASTType.TSetSlot, name, content: slotAst, on, attrs, scope }; + // } + + // // default slot + // const defaultContent = parseChildNodes(clone, ctx); + // slots = slots || {}; + // // t-set-slot="default" has priority over content + // if (defaultContent && !slots.default) { + // slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope }; + // } +} + function parseTTranslation(node: Element, ctx: ParsingContext): AST | null { if (node.getAttribute("t-translation") !== "off") { return null; diff --git a/tests/components/__snapshots__/slots.test.ts.snap b/tests/components/__snapshots__/slots.test.ts.snap index 2a3521ea..6316f19d 100644 --- a/tests/components/__snapshots__/slots.test.ts.snap +++ b/tests/components/__snapshots__/slots.test.ts.snap @@ -282,6 +282,47 @@ exports[`slots can use t-call in default-content of t-slot 3`] = ` }" `; +exports[`slots conditional slot 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { capture, markRaw } = helpers; + const comp1 = app.createComponent(\`Child\`, true, true, false, true); + + function slotFn1(ctx, node, key = \\"\\") { + return text(\`blue\`); + } + + function slotFn2(ctx, node, key = \\"\\") { + return text(\`red\`); + } + + return function template(ctx, node, key = \\"\\") { + let slot1 = {}; + if (ctx['state'].flag) { + const ctx1 = capture(ctx); + slot1['abc'] = {__render: slotFn1.bind(this), __ctx: ctx1}; + } else { + const ctx2 = capture(ctx); + slot1['abc'] = {__render: slotFn2.bind(this), __ctx: ctx2}; + } + const b4 = comp1({slots: markRaw(slot1)}, key + \`__1\`, node, this, null); + } +}" +`; + +exports[`slots conditional slot 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { callSlot } = helpers; + + return function template(ctx, node, key = \\"\\") { + return callSlot(ctx, node, key, 'abc', false, {}); + } +}" +`; + exports[`slots content is the default slot (variation) 1`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/components/slots.test.ts b/tests/components/slots.test.ts index f36ef6d0..6b7c3dbd 100644 --- a/tests/components/slots.test.ts +++ b/tests/components/slots.test.ts @@ -1933,4 +1933,33 @@ describe("slots", () => { "

1 hello

child

2 hello

child

" ); }); + + test.only("conditional slot", async () => { + class Child extends Component { + static template = xml``; + } + + class Parent extends Component { + static template = xml` + + + blue + + + red + + `; + static components = { Child }; + state = useState({ flag: true}); + } + const parent = await mount(Parent, fixture); + + expect(fixture.innerHTML).toBe("blue"); + + parent.state.flag = false; + await nextTick(); + expect(fixture.innerHTML).toBe("red"); + + }); + });