diff --git a/doc/reference/qweb_templating_language.md b/doc/reference/qweb_templating_language.md
index e468984d..5c6d17e8 100644
--- a/doc/reference/qweb_templating_language.md
+++ b/doc/reference/qweb_templating_language.md
@@ -13,6 +13,7 @@
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes)
+ - [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
@@ -76,6 +77,7 @@ needs. Here is a list of all Owl specific directives:
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
+| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## Reference
@@ -324,6 +326,22 @@ values) or a pair `[key, value]`. For example:
```
+### Dynamic tag names
+
+When writing generic components or templates, the specific concrete tag for an
+HTML element is not known yet. In those situations, the `t-tag` directive is
+useful. It simply evaluates dynamically an expression to use as a tag name. The
+template:
+
+```xml
+
+ content
+
+```
+
+will be rendered as `content
` if the `tag` context key
+is set to `div`.
+
### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the
diff --git a/src/qweb/qweb.ts b/src/qweb/qweb.ts
index cf6cce9c..4222366a 100644
--- a/src/qweb/qweb.ts
+++ b/src/qweb/qweb.ts
@@ -203,6 +203,7 @@ export class QWeb extends EventBus {
att: 1,
attf: 1,
translation: 1,
+ tag: 1,
};
static DIRECTIVES: Directive[] = [];
@@ -614,7 +615,7 @@ export class QWeb extends EventBus {
}
}
- if (node.nodeName !== "t") {
+ if (node.nodeName !== "t" || node.hasAttribute("t-tag")) {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
let nodeHooks = {};
@@ -841,7 +842,14 @@ export class QWeb extends EventBus {
ctx.addLine(`}`);
ctx.closeIf();
}
- ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
+ let nodeName = `'${node.nodeName}'`;
+ if ((node).hasAttribute("t-tag")) {
+ const tagExpr = (node).getAttribute("t-tag");
+ (node).removeAttribute("t-tag");
+ nodeName = `tag${ctx.generateID()}`;
+ ctx.addLine(`let ${nodeName} = ${ctx.formatExpression(tagExpr)};`);
+ }
+ ctx.addLine(`let vn${nodeID} = h(${nodeName}, p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} else if (ctx.loopNumber || ctx.hasKey0) {
diff --git a/tests/qweb/__snapshots__/qweb_t_tag.test.ts.snap b/tests/qweb/__snapshots__/qweb_t_tag.test.ts.snap
new file mode 100644
index 00000000..05ce570c
--- /dev/null
+++ b/tests/qweb/__snapshots__/qweb_t_tag.test.ts.snap
@@ -0,0 +1,71 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`qweb t-tag simple usecases 1`] = `
+"function anonymous(context, extra
+) {
+ // Template name: \\"test\\"
+ let scope = Object.create(context);
+ let result;
+ let h = this.h;
+ let c1 = [], p1 = {key:1};
+ let tag2 = 'div';
+ let vn1 = h(tag2, p1, c1);
+ result = vn1;
+ return result;
+}"
+`;
+
+exports[`qweb t-tag simple usecases 2`] = `
+"function anonymous(context, extra
+) {
+ // Template name: \\"test\\"
+ let scope = Object.create(context);
+ let result;
+ let h = this.h;
+ let c3 = [], p3 = {key:3};
+ let tag4 = scope['tag'];
+ let vn3 = h(tag4, p3, c3);
+ result = vn3;
+ c3.push({text: \`text\`});
+ return result;
+}"
+`;
+
+exports[`qweb t-tag with multiple attributes 1`] = `
+"function anonymous(context, extra
+) {
+ // Template name: \\"test\\"
+ let scope = Object.create(context);
+ let result;
+ let h = this.h;
+ let _2 = {'blueberry':true};
+ let _3 = 'raspberry';
+ let c4 = [], p4 = {key:4,attrs:{taste: _3},class:_2};
+ let tag5 = scope['tag'];
+ let vn4 = h(tag5, p4, c4);
+ result = vn4;
+ c4.push({text: \`gooseberry\`});
+ return result;
+}"
+`;
+
+exports[`qweb t-tag with multiple child nodes 1`] = `
+"function anonymous(context, extra
+) {
+ // Template name: \\"test\\"
+ let scope = Object.create(context);
+ let result;
+ let h = this.h;
+ let c1 = [], p1 = {key:1};
+ let tag2 = scope['tag'];
+ let vn1 = h(tag2, p1, c1);
+ result = vn1;
+ c1.push({text: \` pear \`});
+ let c3 = [], p3 = {key:3};
+ let vn3 = h('span', p3, c3);
+ c1.push(vn3);
+ c3.push({text: \`apple\`});
+ c1.push({text: \` strawberry \`});
+ return result;
+}"
+`;
diff --git a/tests/qweb/qweb_t_tag.test.ts b/tests/qweb/qweb_t_tag.test.ts
new file mode 100644
index 00000000..e39d9400
--- /dev/null
+++ b/tests/qweb/qweb_t_tag.test.ts
@@ -0,0 +1,42 @@
+import { QWeb } from "../../src/qweb/index";
+import { renderToString } from "../helpers";
+
+//------------------------------------------------------------------------------
+// Setup and helpers
+//------------------------------------------------------------------------------
+
+function render(template, context = {}) {
+ const qweb = new QWeb();
+ qweb.addTemplate("test", template);
+ return renderToString(qweb, "test", context);
+}
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+describe("qweb t-tag", () => {
+ test("simple usecases", () => {
+ expect(render(``)).toBe("");
+ expect(render(`text`, { tag: "span" })).toBe("text");
+ });
+
+ test("with multiple child nodes", () => {
+ const template = `
+
+ pear
+ apple
+ strawberry
+ `;
+ expect(render(template, { tag: "div" })).toBe(
+ " pear apple strawberry
"
+ );
+ });
+
+ test("with multiple attributes", () => {
+ const template = `
+ gooseberry`;
+ const expected = `gooseberry
`;
+ expect(render(template, { tag: "div" })).toBe(expected);
+ });
+});