This commit is contained in:
Géry Debongnie
2023-02-20 17:04:49 +01:00
parent 276c8a0295
commit 56cfc6403d
4 changed files with 208 additions and 101 deletions
+52 -33
View File
@@ -15,6 +15,7 @@ import {
ASTLog, ASTLog,
ASTMulti, ASTMulti,
ASTSlot, ASTSlot,
ASTSlotDefinition,
ASTTCall, ASTTCall,
ASTTCallBlock, ASTTCallBlock,
ASTTEsc, ASTTEsc,
@@ -137,12 +138,14 @@ interface Context {
index: number | string; index: number | string;
forceNewBlock: boolean; forceNewBlock: boolean;
preventRoot?: boolean; preventRoot?: boolean;
ignoreRoot?: boolean;
isLast?: boolean; isLast?: boolean;
translate: boolean; translate: boolean;
tKeyExpr: string | null; tKeyExpr: string | null;
nameSpace?: string; nameSpace?: string;
tModelSelectedExpr?: string; tModelSelectedExpr?: string;
ctxVar?: string; ctxVar?: string;
slotVar?: string;
} }
function createContext(parentCtx: Context, params?: Partial<Context>): Context { function createContext(parentCtx: Context, params?: Partial<Context>): Context {
@@ -155,6 +158,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
tKeyExpr: null, tKeyExpr: null,
nameSpace: parentCtx.nameSpace, nameSpace: parentCtx.nameSpace,
tModelSelectedExpr: parentCtx.tModelSelectedExpr, tModelSelectedExpr: parentCtx.tModelSelectedExpr,
slotVar: parentCtx.slotVar
}, },
params params
); );
@@ -384,6 +388,9 @@ export class CodeGenerator {
blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`; blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
} }
if (ctx.ignoreRoot) {
return;
}
if (block.isRoot && !ctx.preventRoot) { if (block.isRoot && !ctx.preventRoot) {
if (this.target.on) { if (this.target.on) {
blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on); blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
@@ -465,6 +472,8 @@ export class CodeGenerator {
return this.compileLog(ast, ctx); return this.compileLog(ast, ctx);
case ASTType.TSlot: case ASTType.TSlot:
return this.compileTSlot(ast, ctx); return this.compileTSlot(ast, ctx);
case ASTType.TSetSlot:
return this.compileTSetSlot(ast, ctx);
case ASTType.TTranslation: case ASTType.TTranslation:
return this.compileTTranslation(ast, ctx); return this.compileTTranslation(ast, ctx);
case ASTType.TPortal: case ASTType.TPortal:
@@ -1144,52 +1153,31 @@ export class CodeGenerator {
const props: string[] = ast.props ? this.formatPropObject(ast.props) : []; const props: string[] = ast.props ? this.formatPropObject(ast.props) : [];
// slots // slots
let slotDef: string = ""; let slotVar: string = "";
if (ast.slots) { if (ast.body) {
let ctxStr = "ctx"; slotVar = generateId("slot");
if (this.target.loopLevel || !this.hasSafeContext) { const subCtx = createContext(ctx, { slotVar, ignoreRoot: true });
ctxStr = generateId("ctx"); this.target.addLine(`let ${slotVar} = {};`);
this.helpers.add("capture"); this.compileAST(ast.body, subCtx);
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(", ")}}`;
} }
if (slotDef && !(ast.dynamicProps || hasSlotsProp)) { if (slotVar && !(ast.dynamicProps || hasSlotsProp)) {
this.helpers.add("markRaw"); this.helpers.add("markRaw");
props.push(`slots: markRaw(${slotDef})`); props.push(`slots: markRaw(${slotVar})`);
} }
let propString = this.getPropString(props, ast.dynamicProps); let propString = this.getPropString(props, ast.dynamicProps);
let propVar: string; let propVar: string;
if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) { if ((slotVar && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
propVar = generateId("props"); propVar = generateId("props");
this.define(propVar!, propString); this.define(propVar!, propString);
propString = propVar!; propString = propVar!;
} }
if (slotDef && (ast.dynamicProps || hasSlotsProp)) { if (slotVar && (ast.dynamicProps || hasSlotsProp)) {
this.helpers.add("markRaw"); 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 // cmap key
@@ -1220,7 +1208,7 @@ export class CodeGenerator {
id, id,
expr: `app.createComponent(${ expr: `app.createComponent(${
ast.isDynamic ? null : expr ast.isDynamic ? null : expr
}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, ${ }, ${!ast.isDynamic}, ${!!ast.body}, ${!!ast.dynamicProps}, ${
!ast.props && !ast.dynamicProps !ast.props && !ast.dynamicProps
})`, })`,
}); });
@@ -1263,6 +1251,37 @@ export class CodeGenerator {
return `${name}(${expr}, [${handlers.join(",")}])`; 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 { compileTSlot(ast: ASTSlot, ctx: Context): string {
this.helpers.add("callSlot"); this.helpers.add("callSlot");
let { block } = ctx; let { block } = ctx;
+86 -68
View File
@@ -23,6 +23,7 @@ export const enum ASTType {
TDebug, TDebug,
TLog, TLog,
TSlot, TSlot,
TSetSlot,
TCallBlock, TCallBlock,
TTranslation, TTranslation,
TPortal, TPortal,
@@ -120,7 +121,9 @@ export interface ASTTCall {
context: string | null; context: string | null;
} }
interface SlotDefinition { export interface ASTSlotDefinition {
type: ASTType.TSetSlot;
name: string;
content: AST | null; content: AST | null;
scope: string | null; scope: string | null;
on: EventHandlers | null; on: EventHandlers | null;
@@ -134,7 +137,8 @@ export interface ASTComponent {
dynamicProps: string | null; dynamicProps: string | null;
on: EventHandlers | null; on: EventHandlers | null;
props: { [name: string]: string } | null; props: { [name: string]: string } | null;
slots: { [name: string]: SlotDefinition } | null; body: AST | null;
// slots: { [name: string]: ASTSlotDefinition } | null;
} }
export interface ASTSlot { export interface ASTSlot {
@@ -186,6 +190,7 @@ export type AST =
| ASTTKey | ASTTKey
| ASTComponent | ASTComponent
| ASTSlot | ASTSlot
| ASTSlotDefinition
| ASTTCallBlock | ASTTCallBlock
| ASTLog | ASTLog
| ASTDebug | ASTDebug
@@ -238,6 +243,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
parseTKey(node, ctx) || parseTKey(node, ctx) ||
parseTTranslation(node, ctx) || parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseTSetSlot(node, ctx) ||
parseTOutNode(node, ctx) || parseTOutNode(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
@@ -573,12 +579,12 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
ast.content = [tcall]; ast.content = [tcall];
return ast; return ast;
} }
if (ast && ast.type === ASTType.TComponent) { // if (ast && ast.type === ASTType.TComponent) {
return { // return {
...ast, // ...ast,
slots: { default: { content: tcall, scope: null, on: null, attrs: null } }, // slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
}; // };
} // }
} }
const body = parseChildren(node, ctx); const body = parseChildren(node, ctx);
@@ -706,8 +712,8 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
const dynamicProps = node.getAttribute("t-props"); const dynamicProps = node.getAttribute("t-props");
node.removeAttribute("t-props"); node.removeAttribute("t-props");
const defaultSlotScope = node.getAttribute("t-slot-scope"); // const defaultSlotScope = node.getAttribute("t-slot-scope");
node.removeAttribute("t-slot-scope"); // node.removeAttribute("t-slot-scope");
let on: ASTComponent["on"] = null; let on: ASTComponent["on"] = null;
let props: ASTComponent["props"] = 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()) { if (node.hasChildNodes()) {
const clone = <Element>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 <t> 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 = <Element>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 <t> 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 { function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
if (node.getAttribute("t-translation") !== "off") { if (node.getAttribute("t-translation") !== "off") {
return null; return null;
@@ -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`] = ` exports[`slots content is the default slot (variation) 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
+29
View File
@@ -1933,4 +1933,33 @@ describe("slots", () => {
"<p>1 hello <div>child</div></p><p>2 hello <div>child</div></p>" "<p>1 hello <div>child</div></p><p>2 hello <div>child</div></p>"
); );
}); });
test.only("conditional slot", async () => {
class Child extends Component {
static template = xml`<t t-slot="abc"/>`;
}
class Parent extends Component {
static template = xml`
<Child>
<t t-if="state.flag">
<t t-set-slot="abc">blue</t>
</t>
<t t-else="">
<t t-set-slot="abc">red</t>
</t>
</Child>`;
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");
});
}); });