diff --git a/doc/component.md b/doc/component.md
index c0064408..553f8828 100644
--- a/doc/component.md
+++ b/doc/component.md
@@ -15,6 +15,7 @@
- [`t-mounted` directive](#t-mounted-directive)
- [Props Validation](#props-validation)
- [Keeping References](#keeping-references)
+ - [Slots](#slots)
- [Asynchronous rendering](#asynchronous-rendering)
## Overview
@@ -748,6 +749,66 @@ The `t-ref` directive also accepts dynamic values with string interpolation
this.refs.widget_44;
```
+### Slots
+
+To make generic components, it is useful to be able for a parent widget to *inject*
+some sub template, but still be the owner. For example, a generic dialog widget
+will need to render some content, some footer, but with the parent as the
+rendering context.
+
+This is what *slots* are for.
+
+```xml
+
+
+
+
+
+
+
+```
+
+Slots are defined by the caller, with the `t-set` directive:
+
+```xml
+
+
some widget
+
+
+
hey
+
+
+
+
+
+
+```
+
+In this example, the widget `Dialog` will render the slots `content` and `footer`
+with its parent as rendering context. This means that clicking on the button
+will execute the `doSomething` method on the parent, not on the dialog.
+
+Warning! Slots have a technical constraint: the result of the slot rendering
+should have exactly one root node. So,
+
+```xml
+
+
A
+
B
+
+```
+
+is not allowed. A workaround could be to wrap the content in a div:
+
+```xml
+
+
A
+
B
+
+```
+
### Asynchronous rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
diff --git a/doc/qweb.md b/doc/qweb.md
index 9a68df1d..7f026de2 100644
--- a/doc/qweb.md
+++ b/doc/qweb.md
@@ -73,6 +73,7 @@ needs. Here is a list of all Owl specific directives:
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
+| `t-slot` | [Rendering a slot](component.md#slots) |
## QWeb Engine
@@ -177,10 +178,10 @@ root nodes.
QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced by a lookup in the context (so, the
-widget). For example, `a + b.c(d)` will be converted into:
+widget). For example, `a + b.c(d)` will be converted into:
```js
-context['a'] + context['b'].c(context['d'])
+context["a"] + context["b"].c(context["d"]);
```
It is useful to explain the various rules that applies on these expressions:
diff --git a/src/qweb_core.ts b/src/qweb_core.ts
index 17e14942..75e2615a 100644
--- a/src/qweb_core.ts
+++ b/src/qweb_core.ts
@@ -140,6 +140,11 @@ export class QWeb {
// able to map a qweb instance to a template name.
id = nextID++;
+ // slots contains sub templates defined with t-set inside t-widget nodes, and
+ // are meant to be used by the t-slot directive.
+ slots = {};
+ nextSlotId = 1;
+
constructor(data?: string) {
if (data) {
this.addTemplates(data);
diff --git a/src/qweb_extensions.ts b/src/qweb_extensions.ts
index 81375ea0..1da7d45a 100644
--- a/src/qweb_extensions.ts
+++ b/src/qweb_extensions.ts
@@ -11,6 +11,7 @@ import { QWeb, UTILS } from "./qweb_core";
* - t-transition
* - t-widget/t-keepalive
* - t-mounted
+ * - t-slot
*/
//------------------------------------------------------------------------------
@@ -351,7 +352,7 @@ QWeb.addDirective({
name: "widget",
extraNames: ["props", "keepalive"],
priority: 100,
- atNodeEncounter({ ctx, value, node }): boolean {
+ atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//WIDGET");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
@@ -523,6 +524,21 @@ QWeb.addDirective({
ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
);
+
+ // SLOTS
+ const slotNodes = node.querySelectorAll("[t-set]");
+ if (slotNodes.length) {
+ const slotId = qweb.nextSlotId++;
+ for (let i = 0, length = slotNodes.length; i < length; i++) {
+ const slotNode = slotNodes[i];
+ const key = slotNode.getAttribute("t-set")!;
+ slotNode.removeAttribute("t-set");
+ const slotFn = qweb._compile(`slot_${key}_template`, slotNode);
+ qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
+ }
+ ctx.addLine(`w${widgetID}.__owl__.slotId = ${slotId};`);
+ }
+
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
// FIXME: click to re-add widget during remove transition -> leak
@@ -602,3 +618,21 @@ QWeb.addDirective({
);
}
});
+
+//------------------------------------------------------------------------------
+// t-slot
+//------------------------------------------------------------------------------
+QWeb.addDirective({
+ name: "slot",
+ priority: 80,
+ atNodeEncounter({ ctx, value }): boolean {
+ const slotKey = ctx.generateID();
+ ctx.addLine(
+ `const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`
+ );
+ ctx.addLine(
+ `c${ctx.parentNode}.push(slot${slotKey}(context.__owl__.parent, extra));`
+ );
+ return true;
+ }
+});
diff --git a/tests/__snapshots__/component.test.ts.snap b/tests/__snapshots__/component.test.ts.snap
index 1e74a42f..53fd4178 100644
--- a/tests/__snapshots__/component.test.ts.snap
+++ b/tests/__snapshots__/component.test.ts.snap
@@ -583,3 +583,64 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
return vn1;
}"
`;
+
+exports[`t-slot directive can define and call slots 1`] = `
+"function anonymous(context,extra
+) {
+ let utils = this.utils;
+ let QWeb = this.constructor;
+ let owner = context;
+ var h = this.utils.h;
+ let c1 = [], p1 = {key:1};
+ var vn1 = h('div', p1, c1);
+ //WIDGET
+ let _2_index = c1.length;
+ c1.push(null);
+ let def3;
+ let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
+ let props4 = {};
+ if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
+ if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
+ def3 = w4.__owl__.renderPromise;
+ } else {
+ w4.destroy();
+ w4 = false;
+ }
+ }
+ if (!w4) {
+ let widgetKey4 = \`Dialog\`;
+ let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
+ if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
+ w4 = new W4(owner, props4);
+ context.__owl__.cmap[4] = w4.__owl__.id;
+ w4.__owl__.slotsId = 1;
+ def3 = w4._prepare();
+ def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ } else {
+ def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
+ def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
+ }
+ extra.promises.push(def3);
+ return vn1;
+}"
+`;
+
+exports[`t-slot directive can define and call slots 2`] = `
+"function anonymous(context,extra
+) {
+ var h = this.utils.h;
+ let c1 = [], p1 = {key:1};
+ var vn1 = h('div', p1, c1);
+ let c2 = [], p2 = {key:2};
+ var vn2 = h('div', p2, c2);
+ c1.push(vn2);
+ const slot3 = this.slots[context.__owl__.slotsId + '_' + 'header'];
+ c2.push(slot3(context.__owl__.parent, extra));
+ let c4 = [], p4 = {key:4};
+ var vn4 = h('div', p4, c4);
+ c1.push(vn4);
+ const slot5 = this.slots[context.__owl__.slotsId + '_' + 'footer'];
+ c4.push(slot5(context.__owl__.parent, extra));
+ return vn1;
+}"
+`;
diff --git a/tests/component.test.ts b/tests/component.test.ts
index 12a68bce..c4251729 100644
--- a/tests/component.test.ts
+++ b/tests/component.test.ts
@@ -2613,3 +2613,104 @@ describe("can deduce template from name", () => {
expect(fixture.innerHTML).toBe("Rochefort 10");
});
});
+
+describe("t-slot directive", () => {
+ test("can define and call slots", async () => {
+ env.qweb.addTemplates(`
+
+