mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
basic qweb vdom implementation
This commit is contained in:
@@ -35,4 +35,13 @@ npm run demo:serve
|
||||
dist/demo
|
||||
- *watch* recompile the typescript files as soon as they are changed
|
||||
- *serve* starts a live-server pointing to the dist/demo (which will reload the
|
||||
page whenever the files are changed)
|
||||
page whenever the files are changed)
|
||||
|
||||
## Notes
|
||||
|
||||
Before even thinking about using this in a real scenario:
|
||||
|
||||
- check qweb tests and see if it is reasonable
|
||||
- Note: the compilation of a template should have a unique node (but sub templates
|
||||
can have multiple roots)
|
||||
- remove the "if (${exprID} || ${exprID} === 0) {"
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { Env } from "../../src/core/widget";
|
||||
import QWeb from "../../src/core/qweb";
|
||||
import QWeb from "../../src/core/qweb_vdom";
|
||||
|
||||
const qweb = new QWeb();
|
||||
|
||||
|
||||
+19
-8
@@ -1,11 +1,22 @@
|
||||
///<amd-module name="main" />
|
||||
|
||||
import RootWidget from "./RootWidget";
|
||||
import env from "./env";
|
||||
import QWeb from "../../src/core/qweb_vdom";
|
||||
import {init} from "../../src/libs/snabbdom/src/snabbdom"
|
||||
import h from "../../src/libs/snabbdom/src/h"
|
||||
import sdProps from "../../src/libs/snabbdom/src/modules/props"
|
||||
import sdListeners from "../../src/libs/snabbdom/src/modules/eventlisteners"
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const rootWidget = new RootWidget(null);
|
||||
rootWidget.setEnvironment(env);
|
||||
const mainDiv = document.getElementById("app")!;
|
||||
await rootWidget.mount(mainDiv);
|
||||
});
|
||||
const patch = init([sdProps, sdListeners]);
|
||||
|
||||
(<any>window).h = h;
|
||||
(<any>window).patch = patch;
|
||||
(<any>window).QWeb = QWeb;
|
||||
// import RootWidget from "./RootWidget";
|
||||
// import env from "./env";
|
||||
|
||||
// document.addEventListener("DOMContentLoaded", async function() {
|
||||
// const rootWidget = new RootWidget(null);
|
||||
// rootWidget.setEnvironment(env);
|
||||
// const mainDiv = document.getElementById("app")!;
|
||||
// await rootWidget.mount(mainDiv);
|
||||
// });
|
||||
|
||||
+3
-2
@@ -9,7 +9,7 @@
|
||||
"bundle:core": "tsc -m es6 --lib es2017,dom --target esnext --outdir dist src/bundles/core.ts && rollup dist/bundles/core.js --file dist/bundle-core.js --format iife",
|
||||
"bundle:core:watch": "npm run bundle:core -- --watch",
|
||||
"demo:build": "cp -r demo/ dist/demo && npm run demo:build:js",
|
||||
"demo:build:js": "tsc -m amd --lib es2017,dom --target esnext --outfile dist/demo/main.js demo/src/main.ts",
|
||||
"demo:build:js": "tsc --allowjs -m amd --lib es2017,dom --target esnext --outfile dist/demo/main.js demo/src/main.ts",
|
||||
"predemo:watch": "npm run demo:build",
|
||||
"demo:watch": "npm run demo:build:js -- --watch",
|
||||
"demo:serve": "live-server dist/demo/"
|
||||
@@ -27,11 +27,12 @@
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.3.12",
|
||||
"jest": "^23.6.0",
|
||||
"live-server": "^1.2.1",
|
||||
"rollup": "^1.1.0",
|
||||
"rollup-plugin-typescript2": "^0.19.0",
|
||||
"snabbdom-to-html": "^5.1.1",
|
||||
"source-map-support": "^0.5.10",
|
||||
"ts-jest": "^23.10.5",
|
||||
"live-server": "^1.2.1",
|
||||
"typescript": "^3.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import { escape } from "./utils";
|
||||
import directives from "./qweb_directives";
|
||||
import { Directive } from "./qweb_directives";
|
||||
import {init} from "../libs/snabbdom/src/snabbdom"
|
||||
import sdProps from "../libs/snabbdom/src/modules/props"
|
||||
import sdListeners from "../libs/snabbdom/src/modules/eventlisteners"
|
||||
import h from "../libs/snabbdom/src/h"
|
||||
// const init = require("../libs/snabbdom/dist/snabbdom.js");
|
||||
// import {init} from '../libs/snabbdom/dist/snabbdom.js';
|
||||
|
||||
const patch = init([sdProps, sdListeners]);
|
||||
(<any>window).h = h;
|
||||
(<any>window).patch = patch;
|
||||
|
||||
|
||||
// console.log(init);
|
||||
type RawTemplate = string;
|
||||
type Template = (context: any) => DocumentFragment;
|
||||
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
import { VNode } from "../libs/snabbdom/src/vnode";
|
||||
import h from "../libs/snabbdom/src/h";
|
||||
|
||||
export type EvalContext = { [key: string]: any };
|
||||
type RawTemplate = string;
|
||||
type ParsedTemplate = Document;
|
||||
type CompiledTemplate = (context: EvalContext) => VNode;
|
||||
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
|
||||
","
|
||||
);
|
||||
|
||||
// Compilation Context
|
||||
export class Context {
|
||||
nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: any } = {};
|
||||
escaping: boolean = false;
|
||||
parentNode: number | null = null;
|
||||
rootNode: number | null = null;
|
||||
indentLevel: number = 0;
|
||||
rootContext: Context;
|
||||
caller: Element | undefined;
|
||||
|
||||
constructor() {
|
||||
this.rootContext = this;
|
||||
this.addLine("let h = this.h");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
const id = this.rootContext.nextID++;
|
||||
return id;
|
||||
}
|
||||
|
||||
withParent(node: number): Context {
|
||||
const newContext: Context = Object.create(this);
|
||||
newContext.parentNode = node;
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
return newContext;
|
||||
}
|
||||
|
||||
withVariables(variables: { [key: string]: any }) {
|
||||
const newContext = Object.create(this);
|
||||
newContext.variables = Object.create(variables);
|
||||
return newContext;
|
||||
}
|
||||
|
||||
withCaller(node: Element): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext.caller = node;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
withEscaping(): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext.escaping = true;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
indent() {
|
||||
this.indentLevel++;
|
||||
}
|
||||
|
||||
dedent() {
|
||||
this.indentLevel--;
|
||||
}
|
||||
|
||||
addLine(line: string) {
|
||||
const prefix = new Array(this.indentLevel).join("\t");
|
||||
const lastChar = line[line.length - 1];
|
||||
const suffix = lastChar !== "}" && lastChar !== "{" ? ";" : "";
|
||||
this.code.push(prefix + line + suffix);
|
||||
}
|
||||
|
||||
getValue(val: any): any {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
}
|
||||
|
||||
export default class QWeb {
|
||||
rawTemplates: { [name: string]: RawTemplate } = {};
|
||||
parsedTemplates: { [name: string]: ParsedTemplate } = {};
|
||||
templates: { [name: string]: CompiledTemplate } = {};
|
||||
h = h;
|
||||
exprCache: { [key: string]: string } = {};
|
||||
directives: Directive[] = [];
|
||||
|
||||
constructor() {
|
||||
[
|
||||
forEachDirective,
|
||||
escDirective,
|
||||
rawDirective,
|
||||
setDirective,
|
||||
elseDirective,
|
||||
elifDirective,
|
||||
ifDirective,
|
||||
callDirective
|
||||
].forEach(d => this.addDirective(d));
|
||||
}
|
||||
|
||||
addDirective(dir: Directive) {
|
||||
this.directives.push(dir);
|
||||
this.directives.sort((d1, d2) => d1.priority - d2.priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template to the internal template map. Note that it is not
|
||||
* immediately compiled.
|
||||
*/
|
||||
addTemplate(name: string, template: RawTemplate) {
|
||||
if (name in this.rawTemplates) {
|
||||
return;
|
||||
}
|
||||
this.rawTemplates[name] = template;
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(template, "text/xml");
|
||||
if (!doc.firstChild) {
|
||||
throw new Error("Invalid template (should not be empty)");
|
||||
}
|
||||
if (doc.firstChild.nodeName === "parsererror") {
|
||||
throw new Error("Invalid XML in template");
|
||||
}
|
||||
let tbranch = doc.querySelectorAll("[t-elif], [t-else]");
|
||||
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
||||
let node = tbranch[i];
|
||||
let prevElem = node.previousElementSibling!;
|
||||
let pattr = function(name) {
|
||||
return prevElem.getAttribute(name);
|
||||
};
|
||||
let nattr = function(name) {
|
||||
return +!!node.getAttribute(name);
|
||||
};
|
||||
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
||||
if (pattr("t-foreach")) {
|
||||
throw new Error(
|
||||
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
|
||||
);
|
||||
}
|
||||
if (
|
||||
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) {
|
||||
return a + b;
|
||||
}) > 1
|
||||
) {
|
||||
throw new Error(
|
||||
"Only one conditional branching directive is allowed per node"
|
||||
);
|
||||
}
|
||||
// All text nodes between branch nodes are removed
|
||||
let textNode;
|
||||
while ((textNode = node.previousSibling) !== prevElem) {
|
||||
if (textNode.nodeValue.trim().length) {
|
||||
throw new Error("text is not allowed between branching directives");
|
||||
}
|
||||
textNode.remove();
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.parsedTemplates[name] = doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template
|
||||
*
|
||||
* @param {string} name the template should already have been added
|
||||
*/
|
||||
render(name: string, context: any = {}): VNode {
|
||||
const template = this.templates[name] || this._compile(name);
|
||||
return template(context);
|
||||
}
|
||||
|
||||
_compile(name: string): CompiledTemplate {
|
||||
if (name in this.templates) {
|
||||
return this.templates[name];
|
||||
}
|
||||
|
||||
const doc = this.parsedTemplates[name];
|
||||
|
||||
let ctx = new Context();
|
||||
|
||||
const mainNode = doc.firstChild!;
|
||||
this._compileNode(mainNode, ctx);
|
||||
|
||||
if (!ctx.rootNode) {
|
||||
throw new Error("A template should have one root node");
|
||||
}
|
||||
ctx.addLine(`return vn${ctx.rootNode}`);
|
||||
const functionCode = ctx.code.join("\n");
|
||||
if ((<Element>mainNode).attributes.hasOwnProperty("t-debug")) {
|
||||
console.log(
|
||||
`Template: ${this.rawTemplates[name]}\nCompiled code:\n` + functionCode
|
||||
);
|
||||
}
|
||||
const template: CompiledTemplate = (new Function(
|
||||
"context",
|
||||
functionCode
|
||||
) as CompiledTemplate).bind(this);
|
||||
this.templates[name] = template;
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code from an xml node
|
||||
*
|
||||
*/
|
||||
_compileNode(node: ChildNode, ctx: Context) {
|
||||
if (!(node instanceof Element)) {
|
||||
// this is a text node, there are no directive to apply
|
||||
let text = node.textContent!;
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const attributes = (<Element>node).attributes;
|
||||
|
||||
const validDirectives: {
|
||||
directive: Directive;
|
||||
value: string;
|
||||
fullName: string;
|
||||
}[] = [];
|
||||
|
||||
for (let directive of this.directives) {
|
||||
// const value = attributes[i].textContent!;
|
||||
let fullName;
|
||||
let value;
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const name = attributes[i].name;
|
||||
if (
|
||||
name === "t-" + directive.name ||
|
||||
name.startsWith("t-" + directive.name + "-")
|
||||
) {
|
||||
fullName = name;
|
||||
value = attributes[i].textContent;
|
||||
}
|
||||
}
|
||||
if (fullName) {
|
||||
validDirectives.push({ directive, value, fullName });
|
||||
}
|
||||
}
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.atNodeEncounter) {
|
||||
const isDone = directive.atNodeEncounter({
|
||||
node,
|
||||
qweb: this,
|
||||
ctx,
|
||||
fullName,
|
||||
value
|
||||
});
|
||||
if (isDone) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = this._compileGenericNode(node, ctx);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.atNodeCreation) {
|
||||
directive.atNodeCreation({
|
||||
node,
|
||||
qweb: this,
|
||||
ctx,
|
||||
fullName,
|
||||
value,
|
||||
nodeID
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._compileChildren(node, ctx);
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.finalize) {
|
||||
directive.finalize({ node, qweb: this, ctx, fullName, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_compileGenericNode(node: ChildNode, ctx: Context): number {
|
||||
// nodeType 1 is generic tag
|
||||
if (node.nodeType !== 1) {
|
||||
throw new Error("unsupported node type");
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
const attrs: string[] = [];
|
||||
const tattrs: number[] = [];
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const name = attributes[i].name;
|
||||
const value = attributes[i].textContent!;
|
||||
|
||||
// regular attributes
|
||||
if (!name.startsWith("t-")) {
|
||||
const attID = ctx.generateID();
|
||||
ctx.addLine(`let _${attID} = '${value}'`);
|
||||
attrs.push(`${name}: _${attID}`);
|
||||
}
|
||||
|
||||
// dynamic attributes
|
||||
if (name.startsWith("t-att-")) {
|
||||
const attName = name.slice(6);
|
||||
const formattedValue = this._formatExpression(value!);
|
||||
const attID = ctx.generateID();
|
||||
ctx.addLine(`let _${attID} = ${formattedValue}`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
}
|
||||
|
||||
if (name.startsWith("t-attf-")) {
|
||||
const attName = name.slice(7);
|
||||
const formattedExpr = value!.replace(
|
||||
/\{\{.*?\}\}/g,
|
||||
s => "${" + this._formatExpression(s.slice(2, -2)) + "}"
|
||||
);
|
||||
const attID = ctx.generateID();
|
||||
ctx.addLine(`let _${attID} = \`${formattedExpr}\``);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
}
|
||||
|
||||
// t-att= attributes
|
||||
if (name === "t-att") {
|
||||
let id = ctx.generateID();
|
||||
ctx.addLine(`let _${id} = ${this._formatExpression(value!)}`);
|
||||
tattrs.push(id);
|
||||
}
|
||||
}
|
||||
let nodeID = ctx.generateID();
|
||||
let p =
|
||||
attrs.length + tattrs.length > 0 ? `{attrs:{${attrs.join(",")}}}` : "{}";
|
||||
ctx.addLine(`let c${nodeID} = [], p${nodeID} = ${p}`);
|
||||
|
||||
for (let id of tattrs) {
|
||||
ctx.addLine(`if (_${id} instanceof Array) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1]`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`} else {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`for (let key in _${id}) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key]`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
}
|
||||
ctx.addLine(`
|
||||
let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID})`);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID})`);
|
||||
}
|
||||
|
||||
return nodeID;
|
||||
}
|
||||
|
||||
_compileChildren(node: ChildNode, ctx: Context) {
|
||||
if (node.childNodes.length > 0) {
|
||||
for (let child of Array.from(node.childNodes)) {
|
||||
this._compileNode(child, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_formatExpression(e: string): string {
|
||||
if (e in this.exprCache) {
|
||||
return this.exprCache[e];
|
||||
}
|
||||
// Thanks CHM for this code...
|
||||
const chars = e.split("");
|
||||
let instring = "";
|
||||
let invar = "";
|
||||
let invarPos = 0;
|
||||
let r = "";
|
||||
chars.push(" ");
|
||||
for (var i = 0, ilen = chars.length; i < ilen; i++) {
|
||||
var c = chars[i];
|
||||
if (instring.length) {
|
||||
if (c === instring && chars[i - 1] !== "\\") {
|
||||
instring = "";
|
||||
}
|
||||
} else if (c === '"' || c === "'") {
|
||||
instring = c;
|
||||
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
|
||||
invar = c;
|
||||
invarPos = i;
|
||||
continue;
|
||||
} else if (c.match(/\W/) && invar.length) {
|
||||
// TODO: Should check for possible spaces before dot
|
||||
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
|
||||
invar = "context['" + invar + "']";
|
||||
}
|
||||
r += invar;
|
||||
invar = "";
|
||||
} else if (invar.length) {
|
||||
invar += c;
|
||||
continue;
|
||||
}
|
||||
r += c;
|
||||
}
|
||||
const result = r.slice(0, -1);
|
||||
this.exprCache[e] = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// QWeb Directives
|
||||
//------------------------------------------------------------------------------
|
||||
interface CompilationInfo {
|
||||
nodeID?: number;
|
||||
node: Element;
|
||||
qweb: QWeb;
|
||||
ctx: Context;
|
||||
fullName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Directive {
|
||||
name: string;
|
||||
priority: number;
|
||||
// if return true, then directive is fully applied and there is no need to
|
||||
// keep processing node. Otherwise, we keep going.
|
||||
atNodeEncounter?(info: CompilationInfo): boolean;
|
||||
atNodeCreation?(info: CompilationInfo);
|
||||
finalize?(info: CompilationInfo);
|
||||
}
|
||||
|
||||
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
if (value === "0" && ctx.caller) {
|
||||
qweb._compileNode(ctx.caller, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const exprID = ctx.generateID();
|
||||
ctx.addLine(`let e${exprID} = ${qweb._formatExpression(value)}`);
|
||||
ctx.addLine(`if (e${exprID} || e${exprID} === 0) {`);
|
||||
ctx.indent();
|
||||
let text = `e${exprID}`;
|
||||
|
||||
if (!ctx.parentNode) {
|
||||
throw new Error("Should not have a text node without a parent");
|
||||
}
|
||||
if (ctx.escaping) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${text}})`);
|
||||
} else {
|
||||
ctx.addLine(`p${ctx.parentNode}.hook = {
|
||||
create: (_, n) => n.elm.innerHTML = e${exprID},
|
||||
update: (_, n) => n.elm.innerHTML = e${exprID},
|
||||
}`); // p${ctx.parentNode}.elm.innerHTML = e${exprID}
|
||||
}
|
||||
ctx.dedent();
|
||||
if (node.childNodes.length) {
|
||||
ctx.addLine("} else {");
|
||||
ctx.indent();
|
||||
qweb._compileChildren(node, ctx);
|
||||
ctx.dedent();
|
||||
}
|
||||
ctx.addLine("}");
|
||||
return;
|
||||
}
|
||||
if (value instanceof NodeList) {
|
||||
for (let node of Array.from(value)) {
|
||||
qweb._compileNode(<ChildNode>node, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const escDirective: Directive = {
|
||||
name: "esc",
|
||||
priority: 70,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
}
|
||||
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
||||
compileValueNode(value, node, qweb, ctx.withEscaping());
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const rawDirective: Directive = {
|
||||
name: "raw",
|
||||
priority: 80,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
}
|
||||
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
||||
compileValueNode(value, node, qweb, ctx);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const setDirective: Directive = {
|
||||
name: "set",
|
||||
priority: 60,
|
||||
atNodeEncounter({ node, ctx }): boolean {
|
||||
const variable = node.getAttribute("t-set")!;
|
||||
let value = node.getAttribute("t-value")!;
|
||||
if (value) {
|
||||
ctx.variables[variable] = value;
|
||||
} else {
|
||||
ctx.variables[variable] = node.childNodes;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const ifDirective: Directive = {
|
||||
name: "if",
|
||||
priority: 20,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-if")!);
|
||||
ctx.addLine(`if (${qweb._formatExpression(cond)}) {`);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
}
|
||||
};
|
||||
|
||||
const elifDirective: Directive = {
|
||||
name: "elif",
|
||||
priority: 30,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-elif")!);
|
||||
ctx.addLine(`else if (${qweb._formatExpression(cond)}) {`);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
}
|
||||
};
|
||||
|
||||
const elseDirective: Directive = {
|
||||
name: "else",
|
||||
priority: 40,
|
||||
atNodeEncounter({ ctx }): boolean {
|
||||
ctx.addLine(`else {`);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
}
|
||||
};
|
||||
|
||||
const callDirective: Directive = {
|
||||
name: "call",
|
||||
priority: 50,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
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.parsedTemplates[subTemplate];
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
nodeCopy.removeAttribute("t-call");
|
||||
|
||||
// extract variables from nodecopy
|
||||
const tempCtx = new Context();
|
||||
qweb._compileNode(nodeCopy, tempCtx);
|
||||
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||
const subCtx = ctx.withCaller(nodeCopy).withVariables(vars);
|
||||
|
||||
qweb._compileNode(nodeTemplate.firstChild!, subCtx);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const forEachDirective: Directive = {
|
||||
name: "foreach",
|
||||
priority: 10,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
const elems = node.getAttribute("t-foreach")!;
|
||||
const name = node.getAttribute("t-as")!;
|
||||
let arrayID = ctx.generateID();
|
||||
ctx.addLine(`let _${arrayID} = ${qweb._formatExpression(elems)}`);
|
||||
ctx.addLine(
|
||||
`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`
|
||||
);
|
||||
let keysID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`let _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID})`
|
||||
);
|
||||
let valuesID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`let _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID})`
|
||||
);
|
||||
ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`context.${name}_first = i === 0`);
|
||||
ctx.addLine(`context.${name}_last = i === _${keysID}.length - 1`);
|
||||
ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd'`);
|
||||
ctx.addLine(`context.${name}_index = i`);
|
||||
ctx.addLine(`context.${name} = _${keysID}[i]`);
|
||||
ctx.addLine(`context.${name}_value = _${valuesID}[i]`);
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
nodeCopy.removeAttribute("t-foreach");
|
||||
qweb._compileNode(nodeCopy, ctx);
|
||||
ctx.dedent();
|
||||
ctx.addLine("}");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
+23
-9
@@ -1,4 +1,10 @@
|
||||
import QWeb from "./qweb";
|
||||
import QWeb from "./qweb_vdom";
|
||||
|
||||
// import {init} from "../libs/snabbdom/src/snabbdom"
|
||||
// import sdProps from "../libs/snabbdom/src/modules/props"
|
||||
// import sdListeners from "../libs/snabbdom/src/modules/eventlisteners"
|
||||
|
||||
// const patch = init([sdProps, sdListeners]);
|
||||
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
@@ -60,6 +66,12 @@ export default class Widget {
|
||||
this.env = Object.create(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* DOCSTRIGN
|
||||
*
|
||||
* @param {Object} newState
|
||||
* @memberof Widget
|
||||
*/
|
||||
async updateState(newState: Object) {
|
||||
Object.assign(this.state, newState);
|
||||
await this.render();
|
||||
@@ -70,14 +82,16 @@ export default class Widget {
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async render() {
|
||||
const fragment = await this.env!.qweb.render(this.name, this);
|
||||
this._setElement(fragment.firstChild!);
|
||||
// const vnode = await this.env!.qweb.render(this.name, this);
|
||||
// patch(this.el, vnode);
|
||||
|
||||
// this._setElement(fragment.firstChild!);
|
||||
}
|
||||
|
||||
private _setElement(el: ChildNode) {
|
||||
if (this.el) {
|
||||
this.el.replaceWith(el);
|
||||
}
|
||||
this.el = el;
|
||||
}
|
||||
// private _setElement(el: ChildNode) {
|
||||
// if (this.el) {
|
||||
// this.el.replaceWith(el);
|
||||
// }
|
||||
// this.el = el;
|
||||
// }
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
declare module "snabbdom-to-html"
|
||||
@@ -0,0 +1,520 @@
|
||||
import QWeb, { EvalContext } from "../src/core/qweb_vdom";
|
||||
import { init } from "../src/libs/snabbdom/src/snabbdom";
|
||||
import sdAttributes from "../src/libs/snabbdom/src/modules/attributes";
|
||||
import sdListeners from "../src/libs/snabbdom/src/modules/eventlisteners";
|
||||
|
||||
const patch = init([sdAttributes, sdListeners]);
|
||||
|
||||
function qwebRender(qweb: QWeb, t: string, context: EvalContext = {}): string {
|
||||
const vnode = qweb.render(t, context);
|
||||
const node = document.createElement(vnode.sel!);
|
||||
patch(node, vnode);
|
||||
return node.outerHTML;
|
||||
}
|
||||
|
||||
function renderToString(t: string, context: EvalContext = {}): string {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("test", t);
|
||||
return qwebRender(qweb, "test", context);
|
||||
}
|
||||
|
||||
describe("static templates", () => {
|
||||
test("empty div", () => {
|
||||
const template = "<div></div>";
|
||||
const expected = template;
|
||||
expect(renderToString(template)).toBe(expected);
|
||||
});
|
||||
|
||||
test("div with a text node", () => {
|
||||
const template = "<div>word</div>";
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe(template);
|
||||
});
|
||||
|
||||
test("div with a span child node", () => {
|
||||
const template = "<div><span>word</span></div>";
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe(template);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("invalid xml", () => {
|
||||
const qweb = new QWeb();
|
||||
|
||||
expect(() => qweb.addTemplate("test", "<div>")).toThrow(
|
||||
"Invalid XML in template"
|
||||
);
|
||||
});
|
||||
|
||||
test("template with only text node", () => {
|
||||
const template = `<t>text</t>`;
|
||||
|
||||
expect(() => renderToString(template)).toThrow(
|
||||
"A template should have one root node"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-esc", () => {
|
||||
test("literal", () => {
|
||||
const template = `<span><t t-esc="'ok'"/></span>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<span>ok</span>");
|
||||
});
|
||||
|
||||
test("variable", () => {
|
||||
const template = `<span><t t-esc="var"/></span>`;
|
||||
const result = renderToString(template, { var: "ok" });
|
||||
expect(result).toBe("<span>ok</span>");
|
||||
});
|
||||
|
||||
test.skip("escaping", () => {
|
||||
const template = `<span t-debug="1"><t t-esc="var"/></span>`;
|
||||
const result = renderToString(template, { var: "<ok>" });
|
||||
expect(result).toBe("<span><ok></span>");
|
||||
});
|
||||
|
||||
test("escaping on a node", () => {
|
||||
const template = `<span t-esc="'ok'"/>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<span>ok</span>");
|
||||
});
|
||||
|
||||
test("escaping on a node with a body", () => {
|
||||
const template = `<span t-esc="'ok'">nope</span>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<span>ok</span>");
|
||||
});
|
||||
|
||||
test("escaping on a node with a body, as a default", () => {
|
||||
const template = `<span t-esc="var">nope</span>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<span>nope</span>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-raw", () => {
|
||||
test("literal", () => {
|
||||
const template = `<span><t t-raw="'ok'"/></span>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<span>ok</span>");
|
||||
});
|
||||
|
||||
test("variable", () => {
|
||||
const template = `<span><t t-raw="var"/></span>`;
|
||||
const result = renderToString(template, { var: "ok" });
|
||||
expect(result).toBe("<span>ok</span>");
|
||||
});
|
||||
|
||||
test("not escaping", () => {
|
||||
const template = `<div><t t-raw="var"/></div>`;
|
||||
const result = renderToString(template, { var: "<ok></ok>" });
|
||||
expect(result).toBe("<div><ok></ok></div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-set", () => {
|
||||
test("set from attribute literal", () => {
|
||||
const template = `<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("set from body literal", () => {
|
||||
const template = `<div><t t-set="value">ok</t><t t-esc="value"/></div>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("set from attribute lookup", () => {
|
||||
const template = `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`;
|
||||
const result = renderToString(template, { value: "ok" });
|
||||
expect(result).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("set from body lookup", () => {
|
||||
const template = `<div><t t-set="stuff"><t t-esc="value"/></t><t t-esc="stuff"/></div>`;
|
||||
const result = renderToString(template, { value: "ok" });
|
||||
expect(result).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("set from empty body", () => {
|
||||
const template = `<div><t t-set="stuff"/><t t-esc="stuff"/></div>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("value priority", () => {
|
||||
const template = `<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<div>1</div>");
|
||||
});
|
||||
|
||||
test("evaluate value expression", () => {
|
||||
const template = `<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<div>3</div>");
|
||||
});
|
||||
|
||||
test("evaluate value expression, part 2", () => {
|
||||
const template = `<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`;
|
||||
const result = renderToString(template, { somevariable: 43 });
|
||||
expect(result).toBe("<div>45</div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-if", () => {
|
||||
test("boolean value true condition", () => {
|
||||
const template = `<div><t t-if="condition">ok</t></div>`;
|
||||
const result = renderToString(template, { condition: true });
|
||||
expect(result).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("boolean value false condition", () => {
|
||||
const template = `<div><t t-if="condition">fail</t></div>`;
|
||||
const result = renderToString(template, { condition: false });
|
||||
expect(result).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("boolean value condition missing", () => {
|
||||
const template = `<span><t t-if="condition">fail</t></span>`;
|
||||
const result = renderToString(template);
|
||||
expect(result).toBe("<span></span>");
|
||||
});
|
||||
|
||||
test("boolean value condition elif", () => {
|
||||
const template = `
|
||||
<div><t t-if="color == 'black'">black pearl</t>
|
||||
<t t-elif="color == 'yellow'">yellow submarine</t>
|
||||
<t t-elif="color == 'red'">red is dead</t>
|
||||
<t t-else="">beer</t></div>
|
||||
`;
|
||||
const result = renderToString(template, { color: "red" });
|
||||
expect(result.trim()).toBe("<div>red is dead</div>");
|
||||
});
|
||||
|
||||
test("boolean value condition else", () => {
|
||||
const template = `
|
||||
<div>
|
||||
<span>begin</span>
|
||||
<t t-if="condition">ok</t>
|
||||
<t t-else="">ok-else</t>
|
||||
<span>end</span>
|
||||
</div>
|
||||
`;
|
||||
const result = renderToString(template, { condition: true });
|
||||
expect(result).toBe(
|
||||
"<div>\n <span>begin</span>\n ok\n <span>end</span>\n </div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("boolean value condition false else", () => {
|
||||
const template = `
|
||||
<div><span>begin</span><t t-if="condition">fail</t>
|
||||
<t t-else="">fail-else</t><span>end</span></div>
|
||||
`;
|
||||
const result = renderToString(template, { condition: false });
|
||||
expect(result).toBe(
|
||||
"<div><span>begin</span>fail-else<span>end</span></div>"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("attributes", () => {
|
||||
test("static attributes", () => {
|
||||
const template = `<div foo="a" bar="b" baz="c"/>`;
|
||||
const expected = `<div foo="a" bar="b" baz="c"></div>`;
|
||||
expect(renderToString(template)).toBe(expected);
|
||||
});
|
||||
|
||||
test("static attributes on void elements", () => {
|
||||
const template = `<img src="/test.jpg" alt="Test"/>`;
|
||||
const expected = `<img src="/test.jpg" alt="Test">`;
|
||||
expect(renderToString(template)).toBe(expected);
|
||||
});
|
||||
|
||||
test("dynamic attributes", () => {
|
||||
const template = `<div t-att-foo="'bar'"/>`;
|
||||
const expected = `<div foo="bar"></div>`;
|
||||
expect(renderToString(template)).toBe(expected);
|
||||
});
|
||||
|
||||
test("fixed variable", () => {
|
||||
const template = `<div t-att-foo="value"/>`;
|
||||
const expected = `<div foo="ok"></div>`;
|
||||
expect(renderToString(template, { value: "ok" })).toBe(expected);
|
||||
});
|
||||
|
||||
test("dynamic attribute falsy variable ", () => {
|
||||
const template = `<div t-att-foo="value"/>`;
|
||||
const expected = `<div></div>`;
|
||||
expect(renderToString(template, { value: false })).toBe(expected);
|
||||
});
|
||||
|
||||
test("tuple literal", () => {
|
||||
const template = `<div t-att="['foo', 'bar']"/>`;
|
||||
const expected = `<div foo="bar"></div>`;
|
||||
expect(renderToString(template)).toBe(expected);
|
||||
});
|
||||
|
||||
test("tuple variable", () => {
|
||||
const template = `<div t-att="value"/>`;
|
||||
const expected = `<div foo="bar"></div>`;
|
||||
expect(renderToString(template, { value: ["foo", "bar"] })).toBe(expected);
|
||||
});
|
||||
|
||||
test("object", () => {
|
||||
const template = `<div t-att="value"/>`;
|
||||
const expected = `<div a="1" b="2" c="3"></div>`;
|
||||
expect(renderToString(template, { value: { a: 1, b: 2, c: 3 } })).toBe(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test("format literal", () => {
|
||||
const template = `<div t-attf-foo="bar"/>`;
|
||||
const expected = `<div foo="bar"></div>`;
|
||||
expect(renderToString(template)).toBe(expected);
|
||||
});
|
||||
|
||||
test("format value", () => {
|
||||
const template = `<div t-attf-foo="b{{value}}r"/>`;
|
||||
const expected = `<div foo="bar"></div>`;
|
||||
expect(renderToString(template, { value: "a" })).toBe(expected);
|
||||
});
|
||||
|
||||
test("format expression", () => {
|
||||
const template = `<div t-attf-foo="{{value + 37}}"/>`;
|
||||
const expected = `<div foo="42"></div>`;
|
||||
expect(renderToString(template, { value: 5 })).toBe(expected);
|
||||
});
|
||||
|
||||
test("format multiple", () => {
|
||||
const template = `<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`;
|
||||
const expected = `<div foo="a 0 is 1 of 2 ]"></div>`;
|
||||
expect(renderToString(template, { value1: 0, value2: 1, value3: 2 })).toBe(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
xit("various escapes", () => {
|
||||
// need to think about this... This one does not pass, but I am not sure it is
|
||||
// a correct test
|
||||
const template = `
|
||||
<div foo="<foo"
|
||||
t-att-bar="bar"
|
||||
t-attf-baz="<{{baz}}>"
|
||||
t-att="qux"/>
|
||||
`;
|
||||
const expected = `<div foo="<foo" bar="<bar>" baz="<"<baz>">" qux="<>"></div>`;
|
||||
expect(
|
||||
renderToString(template, { bar: 0, baz: 1, qux: { qux: "<>" } })
|
||||
).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-call (template calling", () => {
|
||||
test("basic caller", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_basic-callee", "<div>ok</div>");
|
||||
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
|
||||
const expected = "<div>ok</div>";
|
||||
expect(qwebRender(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
|
||||
test("t-call not allowed on a non t node", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_basic-callee", "<t>ok</t>");
|
||||
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
|
||||
expect(() => qwebRender(qweb, "caller")).toThrow('Invalid tag');
|
||||
});
|
||||
|
||||
test("with unused body", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_basic-callee", "<div>ok</div>");
|
||||
qweb.addTemplate("caller", '<t t-call="_basic-callee">WHEEE</t>');
|
||||
const expected = "<div>ok</div>";
|
||||
expect(qwebRender(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
|
||||
test("with unused setbody", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_basic-callee", "<div>ok</div>");
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
'<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>'
|
||||
);
|
||||
const expected = "<div>ok</div>";
|
||||
expect(qwebRender(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
|
||||
test("with used body", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_callee-printsbody", '<h1><t t-esc="0"/></h1>');
|
||||
qweb.addTemplate("caller", '<t t-call="_callee-printsbody">ok</t>');
|
||||
const expected = "<h1>ok</h1>";
|
||||
expect(qwebRender(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
|
||||
test("with used set body", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_callee-uses-foo", '<t t-esc="foo"/>');
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
`
|
||||
<span><t t-call="_callee-uses-foo"><t t-set="foo" t-value="'ok'"/></t></span>`
|
||||
);
|
||||
const expected = "<span>ok</span>";
|
||||
expect(qwebRender(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
|
||||
test("inherit context", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_callee-uses-foo", '<t t-esc="foo"/>');
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
`
|
||||
<div><t t-set="foo" t-value="1"/><t t-call="_callee-uses-foo"/></div>`
|
||||
);
|
||||
const expected = "<div>1</div>";
|
||||
expect(qwebRender(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
|
||||
test("scoped parameters", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_basic-callee", `<t>ok</t>`);
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
`
|
||||
<div>
|
||||
<t t-call="_basic-callee">
|
||||
<t t-set="foo" t-value="42"/>
|
||||
</t>
|
||||
<t t-esc="foo"/>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
const expected = "<div>ok</div>";
|
||||
expect(qwebRender(qweb, "caller").replace(/\s/g, '')).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("foreach", () => {
|
||||
test("iterate on items", () => {
|
||||
const template = `
|
||||
<div>
|
||||
<t t-foreach="[3, 2, 1]" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t>
|
||||
</div>`;
|
||||
const expected = `<div>[0:33][1:22][2:11]</div>`;
|
||||
expect(renderToString(template).replace(/\s/g, '')).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate on items (on a element node)", () => {
|
||||
const template = `
|
||||
<div>
|
||||
<span t-foreach="[1, 2]" t-as="item"><t t-esc="item"/></span>
|
||||
</div>`;
|
||||
const expected = `<div><span>1</span><span>2</span></div>`;
|
||||
expect(renderToString(template).replace(/\s/g, '')).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, position", () => {
|
||||
const template = `
|
||||
<div>
|
||||
<t t-foreach="5" t-as="elem">
|
||||
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_parity"/>)
|
||||
</t>
|
||||
</div>`;
|
||||
const expected = `<div>-first(even)-(odd)-(even)-(odd)-last(even)</div>`;
|
||||
expect(renderToString(template).replace(/\s/g, '')).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, integer param", () => {
|
||||
const template = `<div><t t-foreach="3" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t></div>`;
|
||||
const expected = `<div>[0:00][1:11][2:22]</div>`;
|
||||
expect(renderToString(template).replace(/\s/g, '')).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, dict param", () => {
|
||||
const template = `
|
||||
<div>
|
||||
<t t-foreach="value" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/> - <t t-esc="item_parity"/>]
|
||||
</t>
|
||||
</div>`;
|
||||
const expected = `<div>[0:a1-even][1:b2-odd][2:c3-even]</div>`;
|
||||
expect(
|
||||
renderToString(template, { value: { a: 1, b: 2, c: 3 } }).replace(/\s/g, '')
|
||||
).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("misc", () => {
|
||||
test("global", () => {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
|
||||
qweb.addTemplate(
|
||||
"_callee-uses-foo",
|
||||
`<span t-esc="foo">foo default</span>`
|
||||
);
|
||||
qweb.addTemplate(
|
||||
"_callee-asc-toto",
|
||||
`<div t-raw="toto">toto default</div>`
|
||||
);
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="[4,5,6]" t-as="value">
|
||||
<span t-esc="value"/>
|
||||
<t t-call="_callee-asc">
|
||||
<t t-call="_callee-uses-foo">
|
||||
<t t-set="foo" t-value="'aaa'"/>
|
||||
</t>
|
||||
<t t-call="_callee-uses-foo"/>
|
||||
<t t-set="foo" t-value="'bbb'"/>
|
||||
<t t-call="_callee-uses-foo"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="_callee-asc-toto"/>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
const expected = `
|
||||
<div>
|
||||
|
||||
<span>4</span>
|
||||
<año falló="agüero">
|
||||
<span>aaa</span>
|
||||
<span>foo default</span>
|
||||
|
||||
<span>bbb</span>
|
||||
</año>
|
||||
|
||||
<span>5</span>
|
||||
<año falló="agüero">
|
||||
<span>aaa</span>
|
||||
<span>foo default</span>
|
||||
|
||||
<span>bbb</span>
|
||||
</año>
|
||||
|
||||
<span>6</span>
|
||||
<año falló="agüero">
|
||||
<span>aaa</span>
|
||||
<span>foo default</span>
|
||||
|
||||
<span>bbb</span>
|
||||
</año>
|
||||
|
||||
<div>toto default</div>
|
||||
</div>
|
||||
`.trim();
|
||||
expect(qwebRender(qweb, "caller").trim()).toBe(expected);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user