diff --git a/README.md b/README.md index a35ebe84..ee05488c 100644 --- a/README.md +++ b/README.md @@ -44,4 +44,7 @@ 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) {" \ No newline at end of file +- remove the "if (${exprID} || ${exprID} === 0) {" +- style is props? difference between props and attrs +- text node +- t-extend??? \ No newline at end of file diff --git a/demo/src/RootWidget.ts b/demo/src/RootWidget.ts index 36056a15..34364d4e 100644 --- a/demo/src/RootWidget.ts +++ b/demo/src/RootWidget.ts @@ -11,6 +11,10 @@ const template = ` + + + +
`; diff --git a/src/core/qweb_vdom.ts b/src/core/qweb_vdom.ts index fa6ebf30..28d4ab3c 100644 --- a/src/core/qweb_vdom.ts +++ b/src/core/qweb_vdom.ts @@ -33,6 +33,9 @@ export class Context { withParent(node: number): Context { const newContext: Context = Object.create(this); + if (this === this.rootContext && this.parentNode) { + throw new Error('A template should not have more than one root node'); + } newContext.parentNode = node; if (!this.rootContext.rootNode) { this.rootContext.rootNode = node; @@ -171,7 +174,7 @@ export default class QWeb { * * @param {string} name the template should already have been added */ - render(name: string, context: any = {}): VNode { + render(name: string, context: EvalContext = {}): VNode { if (!(name in this.rawTemplates)) { throw new Error(`Template ${name} does not exist`); } @@ -217,7 +220,16 @@ export default class QWeb { 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}\`})`); + if (ctx.parentNode) { + ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`})`); + } else { + // this is an unusual situation: this text node is the result of the + // template rendering. + let nodeID = ctx.generateID(); + ctx.addLine(`let vn${nodeID} = {text: \`${text}\`}`); + ctx.rootContext.rootNode = nodeID; + ctx.rootContext.parentNode = nodeID; + } return; } diff --git a/tests/qweb_vdom.test.ts b/tests/qweb_vdom.test.ts index bba80013..24a2c8a9 100644 --- a/tests/qweb_vdom.test.ts +++ b/tests/qweb_vdom.test.ts @@ -5,44 +5,56 @@ import sdListeners from "../src/libs/snabbdom/src/modules/eventlisteners"; const patch = init([sdAttributes, sdListeners]); +function trim(str: string): string { + return str.replace(/\s/g, ""); +} + function renderToDOM( qweb: QWeb, - t: string, + template: string, context: EvalContext = {} -): HTMLElement { - const vnode = qweb.render(t, context); +): HTMLElement | Text { + const vnode = qweb.render(template, context); + if (vnode.sel === undefined) { + return document.createTextNode(vnode.text!); + } const node = document.createElement(vnode.sel!); patch(node, vnode); return node; } -function qwebRender(qweb: QWeb, t: string, context: EvalContext = {}): string { - const node = renderToDOM(qweb, t, context); - return node.outerHTML; -} -function renderToString(t: string, context: EvalContext = {}): string { - const qweb = new QWeb(); - qweb.addTemplate("test", t); - return qwebRender(qweb, "test", context); +function renderToString( + qweb: QWeb, + t: string, + context: EvalContext = {} +): string { + const node = renderToDOM(qweb, t, context); + return node instanceof Text ? node.textContent! : node.outerHTML; } describe("static templates", () => { + test("simple string", () => { + const qweb = new QWeb(); + qweb.addTemplate("test", "hello vdom"); + expect(renderToString(qweb, "test")).toBe("hello vdom"); + }); + test("empty div", () => { - const template = "
"; - const expected = template; - expect(renderToString(template)).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", "
"); + expect(renderToString(qweb, "test")).toBe("
"); }); test("div with a text node", () => { - const template = "
word
"; - const result = renderToString(template); - expect(result).toBe(template); + const qweb = new QWeb(); + qweb.addTemplate("test", "
word
"); + expect(renderToString(qweb, "test")).toBe("
word
"); }); test("div with a span child node", () => { - const template = "
word
"; - const result = renderToString(template); - expect(result).toBe(template); + const qweb = new QWeb(); + qweb.addTemplate("test", "
word
"); + expect(renderToString(qweb, "test")).toBe("
word
"); }); }); @@ -55,11 +67,12 @@ describe("error handling", () => { ); }); - test("template with only text node", () => { - const template = `text`; + test("template with text node and tag", () => { + const qweb = new QWeb(); + qweb.addTemplate("test", `textother node`); - expect(() => renderToString(template)).toThrow( - "A template should have one root node" + expect(() => renderToString(qweb, "test")).toThrow( + "A template should not have more than one root node" ); }); @@ -71,163 +84,202 @@ describe("error handling", () => { describe("t-esc", () => { test("literal", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe("ok"); + const qweb = new QWeb(); + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test")).toBe("ok"); }); test("variable", () => { - const template = ``; - const result = renderToString(template, { var: "ok" }); - expect(result).toBe("ok"); + const qweb = new QWeb(); + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test", { var: "ok" })).toBe("ok"); }); test.skip("escaping", () => { - const template = ``; - const result = renderToString(template, { var: "" }); - expect(result).toBe("<ok>"); + const qweb = new QWeb(); + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test", { var: "" })).toBe( + "<ok>" + ); }); test("escaping on a node", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe("ok"); + const qweb = new QWeb(); + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test")).toBe("ok"); }); test("escaping on a node with a body", () => { - const template = `nope`; - const result = renderToString(template); - expect(result).toBe("ok"); + const qweb = new QWeb(); + qweb.addTemplate("test", `nope`); + expect(renderToString(qweb, "test")).toBe("ok"); }); test("escaping on a node with a body, as a default", () => { - const template = `nope`; - const result = renderToString(template); - expect(result).toBe("nope"); + const qweb = new QWeb(); + qweb.addTemplate("test", `nope`); + expect(renderToString(qweb, "test")).toBe("nope"); }); }); describe("t-raw", () => { test("literal", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe("ok"); + const qweb = new QWeb(); + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test")).toBe("ok"); }); test("variable", () => { - const template = ``; - const result = renderToString(template, { var: "ok" }); - expect(result).toBe("ok"); + const qweb = new QWeb(); + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test", { var: "ok" })).toBe("ok"); }); test("not escaping", () => { - const template = `
`; - const result = renderToString(template, { var: "" }); - expect(result).toBe("
"); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + expect(renderToString(qweb, "test", { var: "" })).toBe( + "
" + ); }); }); describe("t-set", () => { test("set from attribute literal", () => { - const template = `
`; - const result = renderToString(template); - expect(result).toBe("
ok
"); + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
` + ); + expect(renderToString(qweb, "test")).toBe("
ok
"); }); test("set from body literal", () => { - const template = `
ok
`; - const result = renderToString(template); - expect(result).toBe("
ok
"); + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `ok` + ); + expect(renderToString(qweb, "test")).toBe("ok"); }); test("set from attribute lookup", () => { - const template = `
`; - const result = renderToString(template, { value: "ok" }); - expect(result).toBe("
ok
"); + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
` + ); + expect(renderToString(qweb, "test", { value: "ok" })).toBe("
ok
"); }); test("set from body lookup", () => { - const template = `
`; - const result = renderToString(template, { value: "ok" }); - expect(result).toBe("
ok
"); + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
` + ); + expect(renderToString(qweb, "test", { value: "ok" })).toBe("
ok
"); }); test("set from empty body", () => { - const template = `
`; - const result = renderToString(template); - expect(result).toBe("
"); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + expect(renderToString(qweb, "test")).toBe("
"); }); test("value priority", () => { - const template = `
2
`; - const result = renderToString(template); - expect(result).toBe("
1
"); + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
2
` + ); + expect(renderToString(qweb, "test")).toBe("
1
"); }); test("evaluate value expression", () => { - const template = `
`; - const result = renderToString(template); - expect(result).toBe("
3
"); + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
` + ); + expect(renderToString(qweb, "test")).toBe("
3
"); }); test("evaluate value expression, part 2", () => { - const template = `
`; - const result = renderToString(template, { somevariable: 43 }); - expect(result).toBe("
45
"); + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
` + ); + expect(renderToString(qweb, "test", { somevariable: 43 })).toBe( + "
45
" + ); }); }); describe("t-if", () => { test("boolean value true condition", () => { - const template = `
ok
`; - const result = renderToString(template, { condition: true }); - expect(result).toBe("
ok
"); + const qweb = new QWeb(); + qweb.addTemplate("test", `
ok
`); + expect(renderToString(qweb, "test", { condition: true })).toBe( + "
ok
" + ); }); test("boolean value false condition", () => { - const template = `
fail
`; - const result = renderToString(template, { condition: false }); - expect(result).toBe("
"); + const qweb = new QWeb(); + qweb.addTemplate("test", `
ok
`); + expect(renderToString(qweb, "test", { condition: false })).toBe( + "
" + ); }); test("boolean value condition missing", () => { - const template = `fail`; - const result = renderToString(template); - expect(result).toBe(""); + const qweb = new QWeb(); + qweb.addTemplate("test", `fail`); + expect(renderToString(qweb, "test")).toBe(""); }); test("boolean value condition elif", () => { - const template = ` -
black pearl + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
black pearl yellow submarine red is dead beer
- `; - const result = renderToString(template, { color: "red" }); - expect(result.trim()).toBe("
red is dead
"); + ` + ); + expect(renderToString(qweb, "test", { color: "red" })).toBe( + "
red is dead
" + ); }); test("boolean value condition else", () => { - const template = ` -
+ const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
begin ok ok-else end
- `; - const result = renderToString(template, { condition: true }); - expect(result).toBe( - "
\n begin\n ok\n end\n
" + ` ); + const result = trim(renderToString(qweb, "test", { condition: true })); + expect(result).toBe("
beginokend
"); }); test("boolean value condition false else", () => { - const template = ` -
beginfail - fail-elseend
- `; - const result = renderToString(template, { condition: false }); + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
beginfail + fail-elseend
+ ` + ); + const result = trim(renderToString(qweb, "test", { condition: false })); expect(result).toBe( "
beginfail-elseend
" ); @@ -236,94 +288,118 @@ describe("t-if", () => { describe("attributes", () => { test("static attributes", () => { - const template = `
`; + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test"); const expected = `
`; - expect(renderToString(template)).toBe(expected); + expect(result).toBe(expected); }); test("static attributes on void elements", () => { - const template = `Test`; - const expected = `Test`; - expect(renderToString(template)).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `Test`); + const result = renderToString(qweb, "test"); + expect(result).toBe(`Test`); }); test("dynamic attributes", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template)).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test"); + expect(result).toBe(`
`); }); test("fixed variable", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: "ok" })).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test", { value: "ok" }); + expect(result).toBe(`
`); }); test("dynamic attribute falsy variable ", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: false })).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test", { value: false }); + expect(result).toBe(`
`); }); test("tuple literal", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template)).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test"); + expect(result).toBe(`
`); }); test("tuple variable", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: ["foo", "bar"] })).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test", { value: ["foo", "bar"] }); + expect(result).toBe(`
`); }); test("object", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: { a: 1, b: 2, c: 3 } })).toBe( - expected - ); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test", { + value: { a: 1, b: 2, c: 3 } + }); + expect(result).toBe(`
`); }); test("format literal", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template)).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test"); + expect(result).toBe(`
`); }); test("format value", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: "a" })).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test", { value: "a" }); + expect(result).toBe(`
`); }); test("format expression", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: 5 })).toBe(expected); + const qweb = new QWeb(); + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test", { value: 5 }); + expect(result).toBe(`
`); }); test("format multiple", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value1: 0, value2: 1, value3: 2 })).toBe( - expected + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
` ); + const result = renderToString(qweb, "test", { + value1: 0, + value2: 1, + value3: 2 + }); + expect(result).toBe(`
`); }); test.skip("various escapes", () => { - // need to think about this... This one does not pass, but I am not sure it is - // a correct test - const template = ` -
- `; + // not needed?? + const qweb = new QWeb(); + qweb.addTemplate( + "test", + ` +
+ ` + ); + const result = renderToString(qweb, "test", { + bar: 0, + baz: 1, + qux: { qux: "<>" } + }); const expected = `
`; - expect( - renderToString(template, { bar: 0, baz: 1, qux: { qux: "<>" } }) - ).toBe(expected); + expect(result).toBe(expected); }); }); @@ -333,14 +409,14 @@ describe("t-call (template calling", () => { qweb.addTemplate("_basic-callee", "
ok
"); qweb.addTemplate("caller", ''); const expected = "
ok
"; - expect(qwebRender(qweb, "caller")).toBe(expected); + expect(renderToString(qweb, "caller")).toBe(expected); }); test("t-call not allowed on a non t node", () => { const qweb = new QWeb(); qweb.addTemplate("_basic-callee", "ok"); qweb.addTemplate("caller", '
'); - expect(() => qwebRender(qweb, "caller")).toThrow("Invalid tag"); + expect(() => renderToString(qweb, "caller")).toThrow("Invalid tag"); }); test("with unused body", () => { @@ -348,7 +424,7 @@ describe("t-call (template calling", () => { qweb.addTemplate("_basic-callee", "
ok
"); qweb.addTemplate("caller", 'WHEEE'); const expected = "
ok
"; - expect(qwebRender(qweb, "caller")).toBe(expected); + expect(renderToString(qweb, "caller")).toBe(expected); }); test("with unused setbody", () => { @@ -359,7 +435,7 @@ describe("t-call (template calling", () => { '' ); const expected = "
ok
"; - expect(qwebRender(qweb, "caller")).toBe(expected); + expect(renderToString(qweb, "caller")).toBe(expected); }); test("with used body", () => { @@ -367,7 +443,7 @@ describe("t-call (template calling", () => { qweb.addTemplate("_callee-printsbody", '

'); qweb.addTemplate("caller", 'ok'); const expected = "

ok

"; - expect(qwebRender(qweb, "caller")).toBe(expected); + expect(renderToString(qweb, "caller")).toBe(expected); }); test("with used set body", () => { @@ -379,7 +455,7 @@ describe("t-call (template calling", () => { ` ); const expected = "ok"; - expect(qwebRender(qweb, "caller")).toBe(expected); + expect(renderToString(qweb, "caller")).toBe(expected); }); test("inherit context", () => { @@ -391,7 +467,7 @@ describe("t-call (template calling", () => {
` ); const expected = "
1
"; - expect(qwebRender(qweb, "caller")).toBe(expected); + expect(renderToString(qweb, "caller")).toBe(expected); }); test("scoped parameters", () => { @@ -409,64 +485,81 @@ describe("t-call (template calling", () => { ` ); const expected = "
ok
"; - expect(qwebRender(qweb, "caller").replace(/\s/g, "")).toBe(expected); + expect(trim(renderToString(qweb, "caller"))).toBe(expected); }); }); describe("foreach", () => { test("iterate on items", () => { - const template = ` + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
[: ] -
`; +
` + ); + const result = trim(renderToString(qweb, "test")); const expected = `
[0:33][1:22][2:11]
`; - expect(renderToString(template).replace(/\s/g, "")).toBe(expected); + expect(result).toBe(expected); }); test("iterate on items (on a element node)", () => { - const template = ` + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
-
`; +
` + ); + const result = trim(renderToString(qweb, "test")); const expected = `
12
`; - expect(renderToString(template).replace(/\s/g, "")).toBe(expected); + expect(result).toBe(expected); }); test("iterate, position", () => { - const template = ` + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
- first last () -
`; +
` + ); + const result = trim(renderToString(qweb, "test")); const expected = `
-first(even)-(odd)-(even)-(odd)-last(even)
`; - expect(renderToString(template).replace(/\s/g, "")).toBe(expected); + expect(result).toBe(expected); }); test("iterate, integer param", () => { - const template = `
- [: ] -
`; + const qweb = new QWeb(); + qweb.addTemplate( + "test", + `
+ [: ] +
` + ); + const result = trim(renderToString(qweb, "test")); const expected = `
[0:00][1:11][2:22]
`; - expect(renderToString(template).replace(/\s/g, "")).toBe(expected); + expect(result).toBe(expected); }); test("iterate, dict param", () => { - const template = ` + const qweb = new QWeb(); + qweb.addTemplate('test',`
[: - ] -
`; +
`); + const result = trim(renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })); const expected = `
[0:a1-even][1:b2-odd][2:c3-even]
`; - expect( - renderToString(template, { value: { a: 1, b: 2, c: 3 } }).replace( - /\s/g, - "" - ) - ).toBe(expected); + expect(result).toBe(expected); }); }); @@ -501,37 +594,34 @@ describe("misc", () => {
` ); - const expected = ` + const result = trim(renderToString(qweb, "caller")) + const expected = trim(`
- - 4 - - aaa - foo default - - bbb - - - 5 - - aaa - foo default - - bbb - - - 6 - - aaa - foo default - - bbb - - + 4 + + aaa + foo default + bbb + + + 5 + + aaa + foo default + bbb + + + 6 + + aaa + foo default + bbb + +
toto default
- `.trim(); - expect(qwebRender(qweb, "caller").trim()).toBe(expected); + `); + expect(result).toBe(expected); }); }); @@ -545,7 +635,7 @@ describe("t-on", () => { a = 3; } }); - node.click(); + (node).click(); expect(a).toBe(3); }); @@ -558,7 +648,7 @@ describe("t-on", () => { a = a + n; } }); - node.click(); + (node).click(); expect(a).toBe(6); }); });