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");
+
+ });
+
});