mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] qweb: properly handle t-raw in various situations
closes #325 closes #327
This commit is contained in:
@@ -285,6 +285,11 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
|
|||||||
<p><span>foo</span></p>
|
<p><span>foo</span></p>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
### 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, ...
|
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, ...
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Context } from "./context";
|
import { Context } from "./context";
|
||||||
import { QWebExprVar } from "./expression_parser";
|
import { QWebExprVar } from "./expression_parser";
|
||||||
import { QWeb } from "./qweb";
|
import { QWeb } from "./qweb";
|
||||||
|
import { htmlToVDOM } from "../vdom/html_to_vdom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Directives
|
* Owl QWeb Directives
|
||||||
@@ -25,6 +26,8 @@ QWeb.utils.getFragment = function(str: string): DocumentFragment {
|
|||||||
return temp.content;
|
return temp.content;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QWeb.utils.htmlToVDOM = htmlToVDOM;
|
||||||
|
|
||||||
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||||
if (value === "0" && ctx.caller) {
|
if (value === "0" && ctx.caller) {
|
||||||
qweb._compileNode(ctx.caller, ctx);
|
qweb._compileNode(ctx.caller, ctx);
|
||||||
@@ -60,15 +63,8 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let fragID = ctx.generateID();
|
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
|
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${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});`);
|
|
||||||
}
|
}
|
||||||
if (node.childNodes.length) {
|
if (node.childNodes.length) {
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
|
|||||||
@@ -207,9 +207,7 @@ QWeb.addDirective({
|
|||||||
);
|
);
|
||||||
ctx.addIf(`slot${slotKey}`);
|
ctx.addIf(`slot${slotKey}`);
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
|
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`
|
||||||
ctx.parentNode
|
|
||||||
}, vars: extra.vars, parent: owner}));`
|
|
||||||
);
|
);
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+2
-2
@@ -458,8 +458,8 @@ export class QWeb extends EventBus {
|
|||||||
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
||||||
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
||||||
}
|
}
|
||||||
if (node.tagName !== 't' && (attrName === 't-esc' || attrName === 't-raw')) {
|
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
|
||||||
const tNode = document.createElement('t');
|
const tNode = document.createElement("t");
|
||||||
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
|
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
|
||||||
for (let child of Array.from(node.childNodes)) {
|
for (let child of Array.from(node.childNodes)) {
|
||||||
tNode.appendChild(child);
|
tNode.appendChild(child);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -96,10 +96,10 @@ describe("Context", () => {
|
|||||||
const parent = new Parent(env);
|
const parent = new Parent(env);
|
||||||
await parent.mount(fixture);
|
await parent.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
|
||||||
expect(steps).toEqual(['child']);
|
expect(steps).toEqual(["child"]);
|
||||||
testContext.state.a = 3;
|
testContext.state.a = 3;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(steps).toEqual(['child', 'child']);
|
expect(steps).toEqual(["child", "child"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parent and children subscribed to same context", async () => {
|
test("parent and children subscribed to same context", async () => {
|
||||||
|
|||||||
@@ -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`<div><t t-raw="state.value"/></div>`;
|
||||||
|
state = useState({ value: "<b>content</b>" });
|
||||||
|
}
|
||||||
|
const widget = new TestW(env);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><b>content</b></div>");
|
||||||
|
|
||||||
|
widget.state.value = "<span>other content</span>";
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>other content</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can render list of t-raw ", async () => {
|
||||||
|
class TestW extends Widget {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-foreach="state.items" t-as="item">
|
||||||
|
<t t-esc="item"/>
|
||||||
|
<t t-raw="item"/>
|
||||||
|
</t>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ items: ["<b>one</b>", "<b>two</b>", "<b>tree</b>"] });
|
||||||
|
}
|
||||||
|
const widget = new TestW(env);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><b>one</b><b>one</b><b>two</b><b>two</b><b>tree</b><b>tree</b></div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -469,6 +469,4 @@ describe("hooks", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
|
||||||
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
|
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -630,12 +630,7 @@ exports[`misc global 1`] = `
|
|||||||
c1.push(vn21);
|
c1.push(vn21);
|
||||||
var _22 = context['toto'];
|
var _22 = context['toto'];
|
||||||
if (_22 || _22 === 0) {
|
if (_22 || _22 === 0) {
|
||||||
var frag23 = utils.getFragment(_22)
|
c21.push(...utils.htmlToVDOM(_22));
|
||||||
var p24 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag23, n.elm),
|
|
||||||
}};
|
|
||||||
var vn24 = h('div', p24)
|
|
||||||
c21.push(vn24);
|
|
||||||
} else {
|
} else {
|
||||||
c21.push({text: \`toto default\`});
|
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];
|
p2.on['click'] = extra.handlers['click' + 2];
|
||||||
var _3 = context['html'];
|
var _3 = context['html'];
|
||||||
if (_3 || _3 === 0) {
|
if (_3 || _3 === 0) {
|
||||||
var frag4 = utils.getFragment(_3)
|
c2.push(...utils.htmlToVDOM(_3));
|
||||||
var p5 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag4, n.elm),
|
|
||||||
}};
|
|
||||||
var vn5 = h('div', p5)
|
|
||||||
c2.push(vn5);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1851,12 +1841,7 @@ exports[`t-raw literal 1`] = `
|
|||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = utils.getFragment(_2)
|
c1.push(...utils.htmlToVDOM(_2));
|
||||||
var p4 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
|
||||||
}};
|
|
||||||
var vn4 = h('div', p4)
|
|
||||||
c1.push(vn4);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1871,12 +1856,7 @@ exports[`t-raw not escaping 1`] = `
|
|||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = utils.getFragment(_2)
|
c1.push(...utils.htmlToVDOM(_2));
|
||||||
var p4 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
|
||||||
}};
|
|
||||||
var vn4 = h('div', p4)
|
|
||||||
c1.push(vn4);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1895,12 +1875,7 @@ exports[`t-raw t-raw and another sibling node 1`] = `
|
|||||||
c2.push({text: \`hello\`});
|
c2.push({text: \`hello\`});
|
||||||
var _3 = context['var'];
|
var _3 = context['var'];
|
||||||
if (_3 || _3 === 0) {
|
if (_3 || _3 === 0) {
|
||||||
var frag4 = utils.getFragment(_3)
|
c1.push(...utils.htmlToVDOM(_3));
|
||||||
var p5 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag4, n.elm),
|
|
||||||
}};
|
|
||||||
var vn5 = h('div', p5)
|
|
||||||
c1.push(vn5);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1915,12 +1890,7 @@ exports[`t-raw variable 1`] = `
|
|||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = utils.getFragment(_2)
|
c1.push(...utils.htmlToVDOM(_2));
|
||||||
var p4 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
|
||||||
}};
|
|
||||||
var vn4 = h('div', p4)
|
|
||||||
c1.push(vn4);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
|
|||||||
+13
-11
@@ -1101,16 +1101,18 @@ describe("t-on", () => {
|
|||||||
);
|
);
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
const owner = {
|
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) {
|
onEdit(projectId, ev) {
|
||||||
expect(ev.defaultPrevented).toBe(true);
|
expect(ev.defaultPrevented).toBe(true);
|
||||||
steps.push(projectId);
|
steps.push(projectId);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||||
expect(node.outerHTML).toBe(`<div><a href="#"> Edit Project 1</a><a href="#"> Edit Project 2</a></div>`);
|
expect(node.outerHTML).toBe(
|
||||||
|
`<div><a href="#"> Edit Project 1</a><a href="#"> Edit Project 2</a></div>`
|
||||||
|
);
|
||||||
|
|
||||||
const links = node.querySelectorAll("a")!;
|
const links = node.querySelectorAll("a")!;
|
||||||
links[0].click();
|
links[0].click();
|
||||||
@@ -1124,10 +1126,10 @@ describe("t-on", () => {
|
|||||||
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-esc="text"/></div>`);
|
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-esc="text"/></div>`);
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
const owner = {
|
const owner = {
|
||||||
text: 'Click here',
|
text: "Click here",
|
||||||
onClick() {
|
onClick() {
|
||||||
steps.push('onClick');
|
steps.push("onClick");
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||||
@@ -1135,7 +1137,7 @@ describe("t-on", () => {
|
|||||||
|
|
||||||
node.querySelector("button")!.click();
|
node.querySelector("button")!.click();
|
||||||
|
|
||||||
expect(steps).toEqual(['onClick']);
|
expect(steps).toEqual(["onClick"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-on combined with t-raw", async () => {
|
test("t-on combined with t-raw", async () => {
|
||||||
@@ -1143,10 +1145,10 @@ describe("t-on", () => {
|
|||||||
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-raw="html"/></div>`);
|
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-raw="html"/></div>`);
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
const owner = {
|
const owner = {
|
||||||
html: 'Click <b>here</b>',
|
html: "Click <b>here</b>",
|
||||||
onClick() {
|
onClick() {
|
||||||
steps.push('onClick');
|
steps.push("onClick");
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||||
@@ -1154,7 +1156,7 @@ describe("t-on", () => {
|
|||||||
|
|
||||||
node.querySelector("button")!.click();
|
node.querySelector("button")!.click();
|
||||||
|
|
||||||
expect(steps).toEqual(['onClick']);
|
expect(steps).toEqual(["onClick"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { h, patch } from "../src/vdom";
|
import { h, patch } from "../src/vdom";
|
||||||
|
import { htmlToVDOM } from "../src/vdom/html_to_vdom";
|
||||||
import { init, addNS } from "../src/vdom/vdom";
|
import { init, addNS } from "../src/vdom/vdom";
|
||||||
|
|
||||||
function map(list, fn) {
|
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("<span></span>");
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
elm = patch(vnode0, nodeList[0]).elm;
|
||||||
|
expect(elm.outerHTML).toEqual("<span></span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tag with text", function() {
|
||||||
|
const nodeList = htmlToVDOM("<span>abc</span>");
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
elm = patch(vnode0, nodeList[0]).elm;
|
||||||
|
expect(elm.outerHTML).toEqual("<span>abc</span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tag with attribute", function() {
|
||||||
|
const nodeList = htmlToVDOM(`<span a="1" b="2">abc</span>`);
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
elm = patch(vnode0, nodeList[0]).elm;
|
||||||
|
expect(elm.outerHTML).toEqual(`<span a="1" b="2">abc</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("misc", function() {
|
||||||
|
const nodeList = htmlToVDOM(`<span a="1" b="2">abc<div>1</div></span>`);
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
elm = patch(vnode0, nodeList[0]).elm;
|
||||||
|
expect(elm.outerHTML).toEqual(`<span a="1" b="2">abc<div>1</div></span>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user