diff --git a/doc/qweb.md b/doc/qweb.md index 984aae89..eb4b6dd9 100644 --- a/doc/qweb.md +++ b/doc/qweb.md @@ -285,6 +285,11 @@ rendered with the value `value` set to `foo` in the rendering conte

foo

``` +Note that since the content of the expression is not known beforehand, the `t-raw` +directive has to parse the html (and convert it to a virtual dom structure) for +each rendering. So, it will be much slower than a regular template. It is +therefore advised to limit the use of `t-raw` whenever possible. + ### Setting Variables QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ... diff --git a/src/qweb/base_directives.ts b/src/qweb/base_directives.ts index 5bdaddb8..cf96424f 100644 --- a/src/qweb/base_directives.ts +++ b/src/qweb/base_directives.ts @@ -1,6 +1,7 @@ import { Context } from "./context"; import { QWebExprVar } from "./expression_parser"; import { QWeb } from "./qweb"; +import { htmlToVDOM } from "../vdom/html_to_vdom"; /** * Owl QWeb Directives @@ -25,6 +26,8 @@ QWeb.utils.getFragment = function(str: string): DocumentFragment { return temp.content; }; +QWeb.utils.htmlToVDOM = htmlToVDOM; + function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) { if (value === "0" && ctx.caller) { qweb._compileNode(ctx.caller, ctx); @@ -60,15 +63,8 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) { } } } else { - let fragID = ctx.generateID(); ctx.rootContext.shouldDefineUtils = true; - ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`); - let tempNodeID = ctx.generateID(); - ctx.addLine(`var p${tempNodeID} = {hook: {`); - ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`); - ctx.addLine(`}};`); - ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`); - ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`); + ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`); } if (node.childNodes.length) { ctx.addElse(); diff --git a/src/qweb/extensions.ts b/src/qweb/extensions.ts index 3054ee8d..97dbdb8c 100644 --- a/src/qweb/extensions.ts +++ b/src/qweb/extensions.ts @@ -207,9 +207,7 @@ QWeb.addDirective({ ); ctx.addIf(`slot${slotKey}`); ctx.addLine( - `slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ - ctx.parentNode - }, vars: extra.vars, parent: owner}));` + `slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));` ); ctx.closeIf(); return true; diff --git a/src/qweb/qweb.ts b/src/qweb/qweb.ts index 992ff18a..b5d27fcc 100644 --- a/src/qweb/qweb.ts +++ b/src/qweb/qweb.ts @@ -458,8 +458,8 @@ export class QWeb extends EventBus { if (!(dName in QWeb.DIRECTIVE_NAMES)) { throw new Error(`Unknown QWeb directive: '${attrName}'`); } - if (node.tagName !== 't' && (attrName === 't-esc' || attrName === 't-raw')) { - const tNode = document.createElement('t'); + if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) { + const tNode = document.createElement("t"); tNode.setAttribute(attrName, node.getAttribute(attrName)!); for (let child of Array.from(node.childNodes)) { tNode.appendChild(child); diff --git a/src/vdom/html_to_vdom.ts b/src/vdom/html_to_vdom.ts new file mode 100644 index 00000000..a7cb434c --- /dev/null +++ b/src/vdom/html_to_vdom.ts @@ -0,0 +1,29 @@ +import { VNode, h } from "./vdom"; + +const parser = new DOMParser(); + +export function htmlToVDOM(html: string): VNode[] { + const doc = parser.parseFromString(html, "text/html"); + const result: VNode[] = []; + for (let child of doc.body.childNodes) { + result.push(htmlToVNode(child)); + } + return result; +} + +function htmlToVNode(node: ChildNode): VNode { + if (!(node instanceof Element)) { + return { text: node.textContent! } as VNode; + } + const attrs = {}; + for (let attr of node.attributes) { + attrs[attr.name] = attr.textContent; + } + const children: VNode[] = []; + if (node.hasChildNodes) { + for (let c of node.childNodes) { + children.push(htmlToVNode(c)); + } + } + return h((node as Element).tagName, { attrs }, children); +} diff --git a/tests/Context.test.ts b/tests/Context.test.ts index 706d7000..c5de29a9 100644 --- a/tests/Context.test.ts +++ b/tests/Context.test.ts @@ -96,10 +96,10 @@ describe("Context", () => { const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
12
"); - expect(steps).toEqual(['child']); + expect(steps).toEqual(["child"]); testContext.state.a = 3; await nextTick(); - expect(steps).toEqual(['child', 'child']); + expect(steps).toEqual(["child", "child"]); }); test("parent and children subscribed to same context", async () => { diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index 26eb78ee..a2df7280 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -4293,3 +4293,39 @@ describe("support svg components", () => { ); }); }); + +describe("t-raw in components", () => { + test("update properly on state changes", async () => { + class TestW extends Widget { + static template = xml`
`; + state = useState({ value: "content" }); + } + const widget = new TestW(env); + await widget.mount(fixture); + + expect(fixture.innerHTML).toBe("
content
"); + + widget.state.value = "other content"; + await nextTick(); + expect(fixture.innerHTML).toBe("
other content
"); + }); + + test("can render list of t-raw ", async () => { + class TestW extends Widget { + static template = xml` +
+ + + + +
`; + state = useState({ items: ["one", "two", "tree"] }); + } + const widget = new TestW(env); + await widget.mount(fixture); + + expect(fixture.innerHTML).toBe( + "
<b>one</b>one<b>two</b>two<b>tree</b>tree
" + ); + }); +}); diff --git a/tests/hooks.test.ts b/tests/hooks.test.ts index 46c8b5f9..3e0f8557 100644 --- a/tests/hooks.test.ts +++ b/tests/hooks.test.ts @@ -440,7 +440,7 @@ describe("hooks", () => { steps.push("onWillStart"); }); onWillUpdateProps(nextProps => { - expect(nextProps).toEqual({value: 2}); + expect(nextProps).toEqual({ value: 2 }); steps.push("onWillUpdateProps"); }); } @@ -454,7 +454,7 @@ describe("hooks", () => { class App extends Component { static template = xml`
`; static components = { MyComponent }; - state = useState({ value: 1}); + state = useState({ value: 1 }); } const app = new App(env); @@ -469,6 +469,4 @@ describe("hooks", () => { expect(fixture.innerHTML).toBe("
2
"); expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]); }); - - }); diff --git a/tests/qweb/__snapshots__/qweb.test.ts.snap b/tests/qweb/__snapshots__/qweb.test.ts.snap index 76f8bf4c..92d068c0 100644 --- a/tests/qweb/__snapshots__/qweb.test.ts.snap +++ b/tests/qweb/__snapshots__/qweb.test.ts.snap @@ -630,12 +630,7 @@ exports[`misc global 1`] = ` c1.push(vn21); var _22 = context['toto']; if (_22 || _22 === 0) { - var frag23 = utils.getFragment(_22) - var p24 = {hook: { - insert: n => n.elm.parentNode.replaceChild(frag23, n.elm), - }}; - var vn24 = h('div', p24) - c21.push(vn24); + c21.push(...utils.htmlToVDOM(_22)); } else { c21.push({text: \`toto default\`}); } @@ -1670,12 +1665,7 @@ exports[`t-on t-on combined with t-raw 1`] = ` p2.on['click'] = extra.handlers['click' + 2]; var _3 = context['html']; if (_3 || _3 === 0) { - var frag4 = utils.getFragment(_3) - var p5 = {hook: { - insert: n => n.elm.parentNode.replaceChild(frag4, n.elm), - }}; - var vn5 = h('div', p5) - c2.push(vn5); + c2.push(...utils.htmlToVDOM(_3)); } return vn1; }" @@ -1851,12 +1841,7 @@ exports[`t-raw literal 1`] = ` var vn1 = h('span', p1, c1); var _2 = 'ok'; if (_2 || _2 === 0) { - var frag3 = utils.getFragment(_2) - var p4 = {hook: { - insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), - }}; - var vn4 = h('div', p4) - c1.push(vn4); + c1.push(...utils.htmlToVDOM(_2)); } return vn1; }" @@ -1871,12 +1856,7 @@ exports[`t-raw not escaping 1`] = ` var vn1 = h('div', p1, c1); var _2 = context['var']; if (_2 || _2 === 0) { - var frag3 = utils.getFragment(_2) - var p4 = {hook: { - insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), - }}; - var vn4 = h('div', p4) - c1.push(vn4); + c1.push(...utils.htmlToVDOM(_2)); } return vn1; }" @@ -1895,12 +1875,7 @@ exports[`t-raw t-raw and another sibling node 1`] = ` c2.push({text: \`hello\`}); var _3 = context['var']; if (_3 || _3 === 0) { - var frag4 = utils.getFragment(_3) - var p5 = {hook: { - insert: n => n.elm.parentNode.replaceChild(frag4, n.elm), - }}; - var vn5 = h('div', p5) - c1.push(vn5); + c1.push(...utils.htmlToVDOM(_3)); } return vn1; }" @@ -1915,12 +1890,7 @@ exports[`t-raw variable 1`] = ` var vn1 = h('span', p1, c1); var _2 = context['var']; if (_2 || _2 === 0) { - var frag3 = utils.getFragment(_2) - var p4 = {hook: { - insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), - }}; - var vn4 = h('div', p4) - c1.push(vn4); + c1.push(...utils.htmlToVDOM(_2)); } return vn1; }" diff --git a/tests/qweb/qweb.test.ts b/tests/qweb/qweb.test.ts index 451e20fb..73f72270 100644 --- a/tests/qweb/qweb.test.ts +++ b/tests/qweb/qweb.test.ts @@ -1099,18 +1099,20 @@ describe("t-on", () => { ` ); - const steps:string[] = []; + const steps: string[] = []; const owner = { - projects: [{id: 1, name: 'Project 1'}, {id: 2, name: 'Project 2'}], + projects: [{ id: 1, name: "Project 1" }, { id: 2, name: "Project 2" }], onEdit(projectId, ev) { expect(ev.defaultPrevented).toBe(true); steps.push(projectId); - }, + } }; const node = renderToDOM(qweb, "test", owner, { handlers: [] }); - expect(node.outerHTML).toBe(`
Edit Project 1 Edit Project 2
`); + expect(node.outerHTML).toBe( + `
Edit Project 1 Edit Project 2
` + ); const links = node.querySelectorAll("a")!; links[0].click(); @@ -1122,12 +1124,12 @@ describe("t-on", () => { test("t-on combined with t-esc", async () => { expect.assertions(3); qweb.addTemplate("test", `
`); - const steps:string[] = []; + const steps: string[] = []; const owner = { - text: 'Click here', + text: "Click here", onClick() { - steps.push('onClick'); - }, + steps.push("onClick"); + } }; const node = renderToDOM(qweb, "test", owner, { handlers: [] }); @@ -1135,18 +1137,18 @@ describe("t-on", () => { node.querySelector("button")!.click(); - expect(steps).toEqual(['onClick']); + expect(steps).toEqual(["onClick"]); }); test("t-on combined with t-raw", async () => { expect.assertions(3); qweb.addTemplate("test", `
`); - const steps:string[] = []; + const steps: string[] = []; const owner = { - html: 'Click here', + html: "Click here", onClick() { - steps.push('onClick'); - }, + steps.push("onClick"); + } }; const node = renderToDOM(qweb, "test", owner, { handlers: [] }); @@ -1154,7 +1156,7 @@ describe("t-on", () => { node.querySelector("button")!.click(); - expect(steps).toEqual(['onClick']); + expect(steps).toEqual(["onClick"]); }); }); diff --git a/tests/vdom.test.ts b/tests/vdom.test.ts index 73e90cb7..8803a6c9 100644 --- a/tests/vdom.test.ts +++ b/tests/vdom.test.ts @@ -1,4 +1,5 @@ import { h, patch } from "../src/vdom"; +import { htmlToVDOM } from "../src/vdom/html_to_vdom"; import { init, addNS } from "../src/vdom/vdom"; function map(list, fn) { @@ -1104,3 +1105,52 @@ describe("snabbdom", function() { }); }); }); + +//------------------------------------------------------------------------------ +// Html to vdom +//------------------------------------------------------------------------------ +describe("html to vdom", function() { + let elm, vnode0; + beforeEach(function() { + elm = document.createElement("div"); + vnode0 = elm; + }); + + test("empty strings return empty list", function() { + expect(htmlToVDOM("")).toEqual([]); + }); + + test("just text", function() { + const nodeList = htmlToVDOM("simple text"); + expect(nodeList).toHaveLength(1); + expect(nodeList[0]).toEqual({ text: "simple text" }); + }); + + test("empty tag", function() { + const nodeList = htmlToVDOM(""); + expect(nodeList).toHaveLength(1); + elm = patch(vnode0, nodeList[0]).elm; + expect(elm.outerHTML).toEqual(""); + }); + + test("tag with text", function() { + const nodeList = htmlToVDOM("abc"); + expect(nodeList).toHaveLength(1); + elm = patch(vnode0, nodeList[0]).elm; + expect(elm.outerHTML).toEqual("abc"); + }); + + test("tag with attribute", function() { + const nodeList = htmlToVDOM(`abc`); + expect(nodeList).toHaveLength(1); + elm = patch(vnode0, nodeList[0]).elm; + expect(elm.outerHTML).toEqual(`abc`); + }); + + test("misc", function() { + const nodeList = htmlToVDOM(`abc
1
`); + expect(nodeList).toHaveLength(1); + elm = patch(vnode0, nodeList[0]).elm; + expect(elm.outerHTML).toEqual(`abc
1
`); + }); +});