mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
@@ -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
|
||||
<div t-name="Dialog" class="modal">
|
||||
<div class="modal-title"><t t-esc="props.title"/></div>
|
||||
<div class="modal-content">
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<t t-slot="footer"/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Slots are defined by the caller, with the `t-set` directive:
|
||||
|
||||
```xml
|
||||
<div t-name="SomeWidget">
|
||||
<div>some widget</div>
|
||||
<t t-widget="Dialog" title="Some Dialog">
|
||||
<t t-set="content">
|
||||
<div>hey</div>
|
||||
</t>
|
||||
<t t-set="footer">
|
||||
<button t-on-click="doSomething">ok</button>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
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
|
||||
<t t-set="content">
|
||||
<div>A</div>
|
||||
<div>B</div>
|
||||
</t>
|
||||
```
|
||||
|
||||
is not allowed. A workaround could be to wrap the content in a div:
|
||||
|
||||
```xml
|
||||
<div t-set="content">
|
||||
<div>A</div>
|
||||
<div>B</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Asynchronous rendering
|
||||
|
||||
Working with asynchronous code always adds a lot of complexity to a system. Whenever
|
||||
|
||||
+3
-2
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
+35
-1
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -2613,3 +2613,104 @@ describe("can deduce template from name", () => {
|
||||
expect(fixture.innerHTML).toBe("<span>Rochefort 10</span>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-slot directive", () => {
|
||||
test("can define and call slots", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="Parent">
|
||||
<t t-widget="Dialog">
|
||||
<t t-set="header"><span>header</span></t>
|
||||
<t t-set="footer"><span>footer</span></t>
|
||||
</t>
|
||||
</div>
|
||||
<div t-name="Dialog">
|
||||
<div><t t-slot="header"/></div>
|
||||
<div><t t-slot="footer"/></div>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class Dialog extends Widget {}
|
||||
class Parent extends Widget {
|
||||
widgets = { Dialog };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><div><span>header</span></div><div><span>footer</span></div></div></div>"
|
||||
);
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("slots are rendered with proper context", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="Parent">
|
||||
<span class="counter"><t t-esc="state.val"/></span>
|
||||
<t t-widget="Dialog">
|
||||
<t t-set="footer"><button t-on-click="doSomething">do something</button></t>
|
||||
</t>
|
||||
</div>
|
||||
<span t-name="Dialog"><t t-slot="footer"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Dialog extends Widget {}
|
||||
class Parent extends Widget {
|
||||
widgets = { Dialog };
|
||||
state = { val: 0 };
|
||||
doSomething() {
|
||||
this.state.val++;
|
||||
}
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
|
||||
);
|
||||
|
||||
fixture.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
|
||||
);
|
||||
});
|
||||
|
||||
test("refs are properly bound in slots", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="Parent">
|
||||
<span class="counter"><t t-esc="state.val"/></span>
|
||||
<t t-widget="Dialog">
|
||||
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
|
||||
</t>
|
||||
</div>
|
||||
<span t-name="Dialog"><t t-slot="footer"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Dialog extends Widget {}
|
||||
class Parent extends Widget {
|
||||
widgets = { Dialog };
|
||||
state = { val: 0 };
|
||||
doSomething() {
|
||||
this.state.val++;
|
||||
}
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
|
||||
);
|
||||
|
||||
(<any>parent.refs.myButton).click();
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+122
-2
@@ -233,7 +233,6 @@ const ANIMATION_CSS = `button {
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const LIFECYCLE_DEMO = `class HookWidget extends owl.Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@@ -1082,6 +1081,121 @@ const RESPONSIVE_CSS = `body {
|
||||
}
|
||||
`;
|
||||
|
||||
const SLOTS = `// This example will not work if your browser does not support ESNext class fields
|
||||
|
||||
// We show here how slots can be used to create generic components. In this
|
||||
// example, the Card component is basically only a container, and is created
|
||||
// by giving it slots, inside the t-widget directive.
|
||||
|
||||
class Card extends owl.Component {
|
||||
state = { fullDisplay: true };
|
||||
|
||||
toggleDisplay() {
|
||||
this.state.fullDisplay = !this.state.fullDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
class Counter extends owl.Component {
|
||||
state = {val: 1};
|
||||
|
||||
inc() {
|
||||
this.state.val++;
|
||||
}
|
||||
}
|
||||
|
||||
// Main root widget
|
||||
class App extends owl.Component {
|
||||
widgets = {Card, Counter};
|
||||
state = {a: 1, b: 3};
|
||||
|
||||
inc(key, delta) {
|
||||
this.state[key] += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Application setup
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);`;
|
||||
|
||||
const SLOTS_XML = `<templates>
|
||||
<div t-name="Card" class="card" t-att-class="state.fullDisplay ? 'full' : 'small'">
|
||||
<div class="card-title">
|
||||
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
|
||||
</div>
|
||||
<t t-if="state.fullDisplay">
|
||||
<div class="card-content" >
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<t t-slot="footer"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
|
||||
</div>
|
||||
|
||||
<div t-name="App" class="main">
|
||||
<t t-widget="Card" title="'Title card A'">
|
||||
<t t-set="content">Content of card 1... [<t t-esc="state.a"/>]</t>
|
||||
<t t-set="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
|
||||
</t>
|
||||
<t t-widget="Card" title="'Title card B'">
|
||||
<div t-set="content">
|
||||
<div>Card 2... [<t t-esc="state.b"/>]</div>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
<t t-set="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t>
|
||||
</t>
|
||||
</div>
|
||||
</templates>`;
|
||||
|
||||
const SLOTS_CSS = `.main {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #eeeeee;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
margin: 10px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
.card.full {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.card.small {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
flex: 0 0 25px;
|
||||
font-weight: bold;
|
||||
background-color: darkcyan;
|
||||
color: white;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.card-title button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1 1 auto;
|
||||
padding: 5px;
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
border-top: 1px solid white;
|
||||
}`;
|
||||
|
||||
const EMPTY = `class App extends owl.Component {
|
||||
}
|
||||
|
||||
@@ -1113,7 +1227,7 @@ export const SAMPLES = [
|
||||
description: "Animations",
|
||||
code: ANIMATION,
|
||||
xml: ANIMATION_XML,
|
||||
css: ANIMATION_CSS,
|
||||
css: ANIMATION_CSS
|
||||
},
|
||||
{
|
||||
description: "Lifecycle demo",
|
||||
@@ -1132,6 +1246,12 @@ export const SAMPLES = [
|
||||
css: RESPONSIVE_CSS,
|
||||
xml: RESPONSIVE_XML
|
||||
},
|
||||
{
|
||||
description: "Slots",
|
||||
code: SLOTS,
|
||||
xml: SLOTS_XML,
|
||||
css: SLOTS_CSS
|
||||
},
|
||||
{
|
||||
description: "Empty",
|
||||
code: EMPTY
|
||||
|
||||
Reference in New Issue
Block a user