Skip to content

Commit 8d28359

Browse files
committed
[IMP] qweb: Make t-call parametric, enable lazy XML evaluation
The goal is to apply to OWL the same type of changes that were made in odoo/odoo#197296.
1 parent fb7d25b commit 8d28359

File tree

10 files changed

+2085
-58
lines changed

10 files changed

+2085
-58
lines changed

src/compiler/code_generator.ts

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import {
3232
} from "./parser";
3333
import { OwlError } from "../common/owl_error";
3434

35+
const zero = Symbol("");
36+
3537
type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
3638
const whitespaceRE = /\s+/g;
3739

@@ -279,7 +281,7 @@ export class CodeGenerator {
279281
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
280282
ast: AST;
281283
staticDefs: { id: string; expr: string }[] = [];
282-
slotNames: Set<String> = new Set();
284+
slotNames: Set<String | Symbol> = new Set();
283285
helpers: Set<string> = new Set();
284286

285287
constructor(ast: AST, options: CodeGenOptions) {
@@ -791,12 +793,24 @@ export class CodeGenerator {
791793
return block!.varName;
792794
}
793795

796+
compileZero() {
797+
this.helpers.add("zero");
798+
this.helpers.add("zero2");
799+
this.helpers.add("callLazyBlock");
800+
const isMultiple = this.slotNames.has(zero);
801+
this.slotNames.add(zero);
802+
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
803+
if (isMultiple) {
804+
key = this.generateComponentKey(key);
805+
}
806+
return `zero2 in ctx ? callLazyBlock(ctx[zero2], node, ${key}) : ctx[zero]`;
807+
}
808+
794809
compileTEsc(ast: ASTTEsc, ctx: Context): string {
795810
let { block, forceNewBlock } = ctx;
796811
let expr: string;
797812
if (ast.expr === "0") {
798-
this.helpers.add("zero");
799-
expr = `ctx[zero]`;
813+
expr = this.compileZero();
800814
} else {
801815
expr = compileExpr(ast.expr);
802816
if (ast.defaultValue) {
@@ -824,8 +838,7 @@ export class CodeGenerator {
824838
block = this.createBlock(block, "html", ctx);
825839
let blockStr;
826840
if (ast.expr === "0") {
827-
this.helpers.add("zero");
828-
blockStr = `ctx[zero]`;
841+
blockStr = this.compileZero();
829842
} else if (ast.body) {
830843
let bodyValue = null;
831844
bodyValue = BlockDescription.nextBlockId;
@@ -1044,50 +1057,73 @@ export class CodeGenerator {
10441057

10451058
compileTCall(ast: ASTTCall, ctx: Context): string {
10461059
let { block, forceNewBlock } = ctx;
1060+
1061+
const attrs: string[] = ast.attrs
1062+
? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx)
1063+
: [];
10471064
let ctxVar = ctx.ctxVar || "ctx";
1048-
if (ast.context) {
1065+
if (!ast.attrs && ast.context) {
10491066
ctxVar = generateId("ctx");
10501067
this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
10511068
}
1069+
10521070
const isDynamic = INTERP_REGEXP.test(ast.name);
10531071
const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
10541072
if (block && !forceNewBlock) {
10551073
this.insertAnchor(block);
10561074
}
10571075
block = this.createBlock(block, "multi", ctx);
10581076
if (ast.body) {
1059-
this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
1060-
this.addLine(`${ctxVar}[isBoundary] = 1;`);
1061-
this.helpers.add("isBoundary");
1062-
const subCtx = createContext(ctx, { ctxVar });
1063-
const bl = this.compileMulti({ type: ASTType.Multi, content: ast.body }, subCtx);
1064-
if (bl) {
1065-
this.helpers.add("zero");
1066-
this.addLine(`${ctxVar}[zero] = ${bl};`);
1077+
if (ast.attrs) {
1078+
let ctxStr = "ctx";
1079+
if (this.target.loopLevel || !this.hasSafeContext) {
1080+
ctxStr = generateId("ctx");
1081+
this.helpers.add("capture");
1082+
this.define(ctxStr, `capture(ctx)`);
1083+
}
1084+
const name = this.compileInNewTarget("callBody", ast.body, ctx);
1085+
this.helpers.add("zero2");
1086+
attrs.push(`[zero2]: {__render: ${name}.bind(this), __ctx: ${ctxStr}}`);
1087+
} else {
1088+
this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
1089+
this.addLine(`${ctxVar}[isBoundary] = 1;`);
1090+
this.helpers.add("isBoundary");
1091+
const subCtx = createContext(ctx, { ctxVar });
1092+
const bl = this.compileAST(ast.body, subCtx);
1093+
if (bl) {
1094+
this.helpers.add("zero");
1095+
this.addLine(`${ctxVar}[zero] = ${bl};`);
1096+
}
10671097
}
10681098
}
1069-
1099+
let ctxString = `{${attrs.join(", ")}}`;
1100+
if (ast.attrs && ast.context) {
1101+
const dynCtxVar = generateId("ctx");
1102+
this.addLine(`let ${dynCtxVar} = ${compileExpr(ast.context)};`);
1103+
ctxString = `Object.assign({}, ${dynCtxVar}${attrs.length ? ", " + ctxString : ""})`;
1104+
}
1105+
const ctxExpr = ast.attrs ? ctxString : ctxVar;
10701106
const key = this.generateComponentKey();
10711107
if (isDynamic) {
10721108
const templateVar = generateId("template");
10731109
if (!this.staticDefs.find((d) => d.id === "call")) {
10741110
this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
10751111
}
10761112
this.define(templateVar, subTemplate);
1077-
this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block!, {
1113+
this.insertBlock(`call(this, ${templateVar}, ${ctxExpr}, node, ${key})`, block!, {
10781114
...ctx,
10791115
forceNewBlock: !block,
10801116
});
10811117
} else {
10821118
const id = generateId(`callTemplate_`);
10831119
this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
1084-
this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block!, {
1120+
this.insertBlock(`${id}.call(this, ${ctxExpr}, node, ${key})`, block!, {
10851121
...ctx,
10861122
forceNewBlock: !block,
10871123
});
10881124
}
1089-
if (ast.body && !ctx.isLast) {
1090-
this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`);
1125+
if (!ast.attrs && ast.body && !ctx.isLast) {
1126+
this.addLine(`${ctxExpr} = ${ctxExpr}.__proto__;`);
10911127
}
10921128
return block.varName;
10931129
}

src/compiler/parser.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ export interface ASTTKey extends BaseAST {
126126
export interface ASTTCall extends BaseAST {
127127
type: ASTType.TCall;
128128
name: string;
129-
body: AST[] | null;
129+
attrs: Attrs | null;
130+
attrsTranslationCtx: Attrs | null;
131+
body: AST | null;
130132
context: string | null;
131133
}
132134

@@ -642,9 +644,35 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
642644
node.removeAttribute("t-call");
643645
node.removeAttribute("t-call-context");
644646

647+
let attrs: Attrs | null = null;
648+
let attrsTranslationCtx: Attrs | null = null;
649+
for (let attributeName of node.getAttributeNames()) {
650+
const value = node.getAttribute(attributeName)!;
651+
if (attributeName.startsWith("t-translation-context-")) {
652+
const attrName = attributeName.slice(22);
653+
attrsTranslationCtx = attrsTranslationCtx || {};
654+
attrsTranslationCtx[attrName] = value;
655+
} else {
656+
attrs = attrs || {};
657+
attrs[attributeName] = value;
658+
}
659+
}
660+
645661
if (node.tagName !== "t") {
662+
if (attrs) {
663+
throw new OwlError(
664+
`Directive 't-call' with params can only be used on <t> nodes (used on a <${node.tagName}>)`
665+
);
666+
}
646667
const ast = parseNode(node, ctx);
647-
const tcall: AST = { type: ASTType.TCall, name: subTemplate, body: null, context };
668+
const tcall: AST = {
669+
type: ASTType.TCall,
670+
name: subTemplate,
671+
attrs: null,
672+
attrsTranslationCtx: null,
673+
body: null,
674+
context,
675+
};
648676
if (ast && ast.type === ASTType.DomNode) {
649677
ast.content = [tcall];
650678
return ast;
@@ -664,12 +692,13 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
664692
};
665693
}
666694
}
667-
const body = parseChildren(node, ctx);
668-
695+
const body = parseChildNodes(node, ctx);
669696
return {
670697
type: ASTType.TCall,
671698
name: subTemplate,
672-
body: body.length ? body : null,
699+
attrs,
700+
attrsTranslationCtx,
701+
body,
673702
context,
674703
};
675704
}

src/runtime/template_helpers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ function callSlot(
4747
return slotBDom || text("");
4848
}
4949

50+
function callLazyBlock(slot: any, parent: any, key: string): BDom {
51+
key = key + "__lazy_block";
52+
const { __render, __ctx } = slot;
53+
const slotScope = ObjectCreate(__ctx || {});
54+
const slotBDom = __render ? __render(slotScope, parent, key) : null;
55+
return slotBDom || text("");
56+
}
57+
5058
function capture(ctx: any): any {
5159
const result = ObjectCreate(ctx);
5260
for (let k in ctx) {
@@ -237,8 +245,10 @@ function makeRefWrapper(node: ComponentNode) {
237245
export const helpers = {
238246
withDefault,
239247
zero: Symbol("zero"),
248+
zero2: Symbol("zero2"),
240249
isBoundary,
241250
callSlot,
251+
callLazyBlock,
242252
capture,
243253
withKey,
244254
prepareList,

tests/compiler/__snapshots__/misc.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,13 @@ exports[`misc global 3`] = `
150150
"function anonymous(app, bdom, helpers
151151
) {
152152
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
153-
let { zero } = helpers;
153+
let { zero, zero2, callLazyBlock } = helpers;
154154
155155
let block1 = createBlock(\`<año block-attribute-0=\\"falló\\"><block-child-0/></año>\`);
156156
157157
return function template(ctx, node, key = \\"\\") {
158158
let attr1 = 'agüero';
159-
const b2 = ctx[zero];
159+
const b2 = zero2 in ctx ? callLazyBlock(ctx[zero2], node, key) : ctx[zero];
160160
return block1([attr1], [b2]);
161161
}
162162
}"

0 commit comments

Comments
 (0)