mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
85318b3ae6
With QWeb, we can register globally templates (using the xml tag or the registerTemplate function). However, these templates, once compiled, can generate sub template compiled functions. Before this commit, these sub functions were local to a specific instance. This means that creating a new QWeb instance and rendering a global parent template would crash, since it was unable to find the actual sub function. This commit fixes the issue: the sub functions are now shared statically, but with a unique ID, so we do not have issues with sub functions having a same name in different QWeb instance. closes #701
388 lines
12 KiB
TypeScript
388 lines
12 KiB
TypeScript
import { CompilationContext } from "./compilation_context";
|
|
import { QWeb } from "./qweb";
|
|
import { htmlToVDOM } from "../vdom/html_to_vdom";
|
|
import { QWebVar } from "./expression_parser";
|
|
|
|
/**
|
|
* Owl QWeb Directives
|
|
*
|
|
* This file contains the implementation of most standard QWeb directives:
|
|
* - t-esc
|
|
* - t-raw
|
|
* - t-set/t-value
|
|
* - t-if/t-elif/t-else
|
|
* - t-call
|
|
* - t-foreach/t-as
|
|
* - t-debug
|
|
* - t-log
|
|
*/
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-esc and t-raw
|
|
//------------------------------------------------------------------------------
|
|
QWeb.utils.htmlToVDOM = htmlToVDOM;
|
|
|
|
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
|
|
ctx.rootContext.shouldDefineScope = true;
|
|
if (value === "0") {
|
|
if (ctx.parentNode) {
|
|
// the 'zero' magical symbol is where we can find the result of the rendering
|
|
// of the body of the t-call.
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
const zeroArgs = ctx.escaping
|
|
? `{text: utils.vDomToString(scope[utils.zero])}`
|
|
: `...scope[utils.zero]`;
|
|
ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let exprID: string;
|
|
if (typeof value === "string") {
|
|
exprID = `_${ctx.generateID()}`;
|
|
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`);
|
|
} else {
|
|
exprID = `scope.${value.id}`;
|
|
}
|
|
ctx.addIf(`${exprID} != null`);
|
|
|
|
if (ctx.escaping) {
|
|
let protectID;
|
|
if (value.hasBody) {
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
protectID = ctx.startProtectScope();
|
|
ctx.addLine(
|
|
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
|
|
);
|
|
}
|
|
if (ctx.parentTextNode) {
|
|
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
|
|
} else if (ctx.parentNode) {
|
|
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
|
|
} else {
|
|
let nodeID = ctx.generateID();
|
|
ctx.rootContext.rootNode = nodeID;
|
|
ctx.rootContext.parentTextNode = nodeID;
|
|
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
|
|
if (ctx.rootContext.shouldDefineResult) {
|
|
ctx.addLine(`result = vn${nodeID}`);
|
|
}
|
|
}
|
|
if (value.hasBody) {
|
|
ctx.stopProtectScope(protectID);
|
|
}
|
|
} else {
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
if (value.hasBody) {
|
|
ctx.addLine(
|
|
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
|
|
);
|
|
ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
|
|
} else {
|
|
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
|
|
}
|
|
}
|
|
if (node.childNodes.length) {
|
|
ctx.addElse();
|
|
qweb._compileChildren(node, ctx);
|
|
}
|
|
|
|
ctx.closeIf();
|
|
}
|
|
|
|
QWeb.addDirective({
|
|
name: "esc",
|
|
priority: 70,
|
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
|
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
|
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
|
return true;
|
|
},
|
|
});
|
|
|
|
QWeb.addDirective({
|
|
name: "raw",
|
|
priority: 80,
|
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
|
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
|
compileValueNode(value, node, qweb, ctx);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-set
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "set",
|
|
extraNames: ["value"],
|
|
priority: 60,
|
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
|
ctx.rootContext.shouldDefineScope = true;
|
|
const variable = node.getAttribute("t-set")!;
|
|
let value = node.getAttribute("t-value")!;
|
|
ctx.variables[variable] = ctx.variables[variable] || ({} as QWebVar);
|
|
let qwebvar = ctx.variables[variable];
|
|
const hasBody = node.hasChildNodes();
|
|
|
|
qwebvar.id = variable;
|
|
qwebvar.expr = `scope.${variable}`;
|
|
if (value) {
|
|
const formattedValue = ctx.formatExpression(value);
|
|
let scopeExpr = `scope`;
|
|
if (ctx.protectedScopeNumber) {
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
scopeExpr = `utils.getScope(scope, '${variable}')`;
|
|
}
|
|
ctx.addLine(`${scopeExpr}.${variable} = ${formattedValue};`);
|
|
qwebvar.value = formattedValue;
|
|
}
|
|
|
|
if (hasBody) {
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
if (value) {
|
|
ctx.addIf(`!(${qwebvar.expr})`);
|
|
}
|
|
const tempParentNodeID = ctx.generateID();
|
|
const _parentNode = ctx.parentNode;
|
|
ctx.parentNode = tempParentNodeID;
|
|
|
|
ctx.addLine(`let c${tempParentNodeID} = new utils.VDomArray();`);
|
|
const nodeCopy = node.cloneNode(true) as Element;
|
|
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
|
|
nodeCopy.removeAttribute(attr);
|
|
}
|
|
qweb._compileNode(nodeCopy, ctx);
|
|
|
|
ctx.addLine(`${qwebvar.expr} = c${tempParentNodeID}`);
|
|
qwebvar.value = `c${tempParentNodeID}`;
|
|
qwebvar.hasBody = true;
|
|
|
|
ctx.parentNode = _parentNode;
|
|
if (value) {
|
|
ctx.closeIf();
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-if, t-elif, t-else
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "if",
|
|
priority: 20,
|
|
atNodeEncounter({ node, ctx }): boolean {
|
|
let cond = ctx.getValue(node.getAttribute("t-if")!);
|
|
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id!}`);
|
|
return false;
|
|
},
|
|
finalize({ ctx }) {
|
|
ctx.closeIf();
|
|
},
|
|
});
|
|
|
|
QWeb.addDirective({
|
|
name: "elif",
|
|
priority: 30,
|
|
atNodeEncounter({ node, ctx }): boolean {
|
|
let cond = ctx.getValue(node.getAttribute("t-elif")!);
|
|
ctx.addLine(
|
|
`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`
|
|
);
|
|
ctx.indent();
|
|
return false;
|
|
},
|
|
finalize({ ctx }) {
|
|
ctx.closeIf();
|
|
},
|
|
});
|
|
|
|
QWeb.addDirective({
|
|
name: "else",
|
|
priority: 40,
|
|
atNodeEncounter({ ctx }): boolean {
|
|
ctx.addLine(`else {`);
|
|
ctx.indent();
|
|
return false;
|
|
},
|
|
finalize({ ctx }) {
|
|
ctx.closeIf();
|
|
},
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-call
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "call",
|
|
priority: 50,
|
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
|
// Step 1: sanity checks
|
|
// ------------------------------------------------
|
|
ctx.rootContext.shouldDefineScope = true;
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
if (node.nodeName !== "t") {
|
|
throw new Error("Invalid tag for t-call directive (should be 't')");
|
|
}
|
|
const subTemplate = node.getAttribute("t-call")!;
|
|
const nodeTemplate = qweb.templates[subTemplate];
|
|
if (!nodeTemplate) {
|
|
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
|
|
}
|
|
|
|
// Step 2: compile target template in sub templates
|
|
// ------------------------------------------------
|
|
let subId = qweb.subTemplates[subTemplate];
|
|
if (!subId) {
|
|
subId = QWeb.nextId++;
|
|
qweb.subTemplates[subTemplate] = subId;
|
|
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true);
|
|
QWeb.subTemplates[subId] = subTemplateFn;
|
|
}
|
|
|
|
// Step 3: compile t-call body if necessary
|
|
// ------------------------------------------------
|
|
let hasBody = node.hasChildNodes();
|
|
let protectID;
|
|
if (hasBody) {
|
|
// we add a sub scope to protect the ambient scope
|
|
ctx.addLine(`{`);
|
|
ctx.indent();
|
|
protectID = ctx.startProtectScope();
|
|
const nodeCopy = node.cloneNode(true) as Element;
|
|
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
|
|
nodeCopy.removeAttribute(attr);
|
|
}
|
|
const parentNode = ctx.parentNode;
|
|
ctx.parentNode = "__0";
|
|
// this local scope is intended to trap c__0
|
|
ctx.addLine(`{`);
|
|
ctx.indent();
|
|
ctx.addLine("let c__0 = [];");
|
|
qweb._compileNode(nodeCopy, ctx);
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
ctx.addLine("scope[utils.zero] = c__0;");
|
|
ctx.parentNode = parentNode;
|
|
ctx.dedent();
|
|
ctx.addLine(`}`);
|
|
}
|
|
|
|
// Step 4: add the appropriate function call to current component
|
|
// ------------------------------------------------
|
|
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
|
|
const parentComponent = `utils.getComponent(context)`;
|
|
const key = ctx.generateTemplateKey();
|
|
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
|
|
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
|
|
if (ctx.parentNode) {
|
|
ctx.addLine(
|
|
`this.constructor.subTemplates['${subId}'].call(this, ${callingScope}, ${extra});`
|
|
);
|
|
} else {
|
|
// this is a t-call with no parentnode, we need to extract the result
|
|
ctx.rootContext.shouldDefineResult = true;
|
|
ctx.addLine(`result = []`);
|
|
ctx.addLine(
|
|
`this.constructor.subTemplates['${subId}'].call(this, ${callingScope}, ${extra});`
|
|
);
|
|
ctx.addLine(`result = result[0]`);
|
|
}
|
|
|
|
// Step 5: restore previous scope
|
|
// ------------------------------------------------
|
|
if (hasBody) {
|
|
ctx.stopProtectScope(protectID);
|
|
ctx.dedent();
|
|
ctx.addLine(`}`);
|
|
}
|
|
|
|
return true;
|
|
},
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-foreach
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "foreach",
|
|
extraNames: ["as"],
|
|
priority: 10,
|
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
|
ctx.rootContext.shouldDefineScope = true;
|
|
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
|
|
const elems = node.getAttribute("t-foreach")!;
|
|
const name = node.getAttribute("t-as")!;
|
|
let arrayID = ctx.generateID();
|
|
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
|
|
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
|
|
let keysID = ctx.generateID();
|
|
let valuesID = ctx.generateID();
|
|
ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`);
|
|
ctx.addIf(`!(_${arrayID} instanceof Array)`);
|
|
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
|
|
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
|
|
ctx.closeIf();
|
|
ctx.addLine(`let _length${keysID} = _${keysID}.length;`);
|
|
let varsID = ctx.startProtectScope(true);
|
|
const loopVar = `i${ctx.loopNumber}`;
|
|
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
|
|
ctx.indent();
|
|
|
|
ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
|
|
ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
|
|
ctx.addLine(`scope.${name}_index = ${loopVar}`);
|
|
ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
|
|
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
|
|
const nodeCopy = <Element>node.cloneNode(true);
|
|
let shouldWarn =
|
|
!nodeCopy.hasAttribute("t-key") &&
|
|
node.children.length === 1 &&
|
|
node.children[0].tagName !== "t" &&
|
|
!node.children[0].hasAttribute("t-key");
|
|
if (shouldWarn) {
|
|
console.warn(
|
|
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
|
|
);
|
|
}
|
|
if (nodeCopy.hasAttribute("t-key")) {
|
|
const expr = ctx.formatExpression(nodeCopy.getAttribute("t-key")!);
|
|
ctx.addLine(`let key${ctx.loopNumber} = ${expr};`);
|
|
nodeCopy.removeAttribute("t-key");
|
|
} else {
|
|
ctx.addLine(`let key${ctx.loopNumber} = i${ctx.loopNumber};`);
|
|
}
|
|
|
|
nodeCopy.removeAttribute("t-foreach");
|
|
qweb._compileNode(nodeCopy, ctx);
|
|
ctx.dedent();
|
|
ctx.addLine("}");
|
|
ctx.stopProtectScope(varsID);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-debug
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "debug",
|
|
priority: 1,
|
|
atNodeEncounter({ ctx }) {
|
|
ctx.addLine("debugger;");
|
|
},
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-log
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "log",
|
|
priority: 1,
|
|
atNodeEncounter({ ctx, value }) {
|
|
const expr = ctx.formatExpression(value);
|
|
ctx.addLine(`console.log(${expr})`);
|
|
},
|
|
});
|