mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] slots: process slot params/values like normal props
This commit is contained in:
committed by
Aaron Bohy
parent
ea2ccc5a03
commit
3196b585fd
@@ -210,6 +210,9 @@ use this `Notebook` component:
|
|||||||
</Notebook>
|
</Notebook>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Slot params works like normal props, so one can use the `.bind` suffix to
|
||||||
|
bind a function if needed.
|
||||||
|
|
||||||
## Slot scopes
|
## Slot scopes
|
||||||
|
|
||||||
For other kinds of advanced use cases, the content of a slot may depends on some
|
For other kinds of advanced use cases, the content of a slot may depends on some
|
||||||
@@ -246,3 +249,6 @@ component itself:
|
|||||||
<t t-esc="scope.num"/>
|
<t t-esc="scope.num"/>
|
||||||
</MyComponent>
|
</MyComponent>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Slot values works like normal props, so one can use the `.bind` suffix to
|
||||||
|
bind a function if needed.
|
||||||
|
|||||||
@@ -1050,29 +1050,50 @@ export class CodeGenerator {
|
|||||||
return parts.join("__");
|
return parts.join("__");
|
||||||
}
|
}
|
||||||
|
|
||||||
compileComponent(ast: ASTComponent, ctx: Context) {
|
/**
|
||||||
let { block } = ctx;
|
* Formats a prop name and value into a string suitable to be inserted in the
|
||||||
|
* generated code. For example:
|
||||||
// props
|
*
|
||||||
const props: string[] = [];
|
* Name Value Result
|
||||||
let hasSlotsProp = false;
|
* ---------------------------------------------------------
|
||||||
for (let propName in ast.props) {
|
* "number" "state" "number: ctx['state']"
|
||||||
let propValue = this.captureExpression(ast.props[propName]) || undefined;
|
* "something" "" "something: undefined"
|
||||||
if (propName.includes(".")) {
|
* "some-prop" "state" "'some-prop': ctx['state']"
|
||||||
let [name, suffix] = propName.split(".");
|
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||||
|
*/
|
||||||
|
formatProp(name: string, value: string): string {
|
||||||
|
value = this.captureExpression(value);
|
||||||
|
if (name.includes(".")) {
|
||||||
|
let [_name, suffix] = name.split(".");
|
||||||
if (suffix === "bind") {
|
if (suffix === "bind") {
|
||||||
this.helpers.add("bind");
|
this.helpers.add("bind");
|
||||||
propName = name;
|
name = _name;
|
||||||
propValue = `bind(ctx, ${propValue})`;
|
value = `bind(ctx, ${value || undefined})`;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid prop suffix");
|
throw new Error("Invalid prop suffix");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
propName = /^[a-z_]+$/i.test(propName) ? propName : `'${propName}'`;
|
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||||||
props.push(`${propName}: ${propValue}`);
|
return `${name}: ${value || undefined}`;
|
||||||
if (propName === "slots") {
|
|
||||||
hasSlotsProp = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatPropObject(obj: { [prop: string]: any }): string {
|
||||||
|
const params = [];
|
||||||
|
for (const [n, v] of Object.entries(obj)) {
|
||||||
|
params.push(this.formatProp(n, v));
|
||||||
|
}
|
||||||
|
return params.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
compileComponent(ast: ASTComponent, ctx: Context) {
|
||||||
|
let { block } = ctx;
|
||||||
|
|
||||||
|
// props
|
||||||
|
const hasSlotsProp = "slots" in ast.props;
|
||||||
|
const props: string[] = [];
|
||||||
|
const propExpr = this.formatPropObject(ast.props);
|
||||||
|
if (propExpr) {
|
||||||
|
props.push(propExpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// slots
|
// slots
|
||||||
@@ -1095,9 +1116,7 @@ export class CodeGenerator {
|
|||||||
params.push(`__scope: "${scope}"`);
|
params.push(`__scope: "${scope}"`);
|
||||||
}
|
}
|
||||||
if (ast.slots[slotName].attrs) {
|
if (ast.slots[slotName].attrs) {
|
||||||
for (const [n, v] of Object.entries(ast.slots[slotName].attrs!)) {
|
params.push(this.formatPropObject(ast.slots[slotName].attrs!));
|
||||||
params.push(`${n}: ${compileExpr(v) || undefined}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const slotInfo = `{${params.join(", ")}}`;
|
const slotInfo = `{${params.join(", ")}}`;
|
||||||
slotStr.push(`'${slotName}': ${slotInfo}`);
|
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||||||
@@ -1176,15 +1195,7 @@ export class CodeGenerator {
|
|||||||
slotName = "'" + ast.name + "'";
|
slotName = "'" + ast.name + "'";
|
||||||
}
|
}
|
||||||
|
|
||||||
let scope = null;
|
const scope = ast.attrs ? `{${this.formatPropObject(ast.attrs)}}` : null;
|
||||||
if (ast.attrs) {
|
|
||||||
const params = [];
|
|
||||||
for (const [n, v] of Object.entries(ast.attrs!)) {
|
|
||||||
params.push(`${n}: ${compileExpr(v) || undefined}`);
|
|
||||||
}
|
|
||||||
scope = `{${params.join(", ")}}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ast.defaultContent) {
|
if (ast.defaultContent) {
|
||||||
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||||||
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
|
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
|
||||||
|
|||||||
@@ -75,6 +75,37 @@ exports[`slots can define and call slots 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`slots can define and call slots with bound params 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
let { capture, bind } = helpers;
|
||||||
|
|
||||||
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`abc\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const ctx1 = capture(ctx);
|
||||||
|
return component(\`Child\`, {slots: {'abc': {__render: slot1, __ctx: ctx1, getValue: bind(ctx, ctx['getValue'])}}}, key + \`__1\`, node, ctx);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`slots can define and call slots with bound params 2`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2 = callSlot(ctx, node, key, 'abc', false, {});
|
||||||
|
let b3 = text(ctx['props'].slots['abc'].getValue());
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`slots can define and call slots with params 1`] = `
|
exports[`slots can define and call slots with params 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -424,6 +455,33 @@ exports[`slots default slot next to named slot, with default content 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`slots default slot with params with - in it 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['slotScope']['some-value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`slots default slot with params with - in it 2`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return callSlot(ctx, node, key, 'default', false, {'some-value': ctx['state'].value});
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`slots default slot with slot scope: shorthand syntax 1`] = `
|
exports[`slots default slot with slot scope: shorthand syntax 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -1207,6 +1265,33 @@ exports[`slots simple default slot with params 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`slots simple default slot with params and bound function 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['slotScope'].fn());
|
||||||
|
}
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`slots simple default slot with params and bound function 2`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
let { callSlot, bind } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return callSlot(ctx, node, key, 'default', false, {fn: bind(ctx, ctx['getValue'])});
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`slots simple default slot, variation 1`] = `
|
exports[`slots simple default slot, variation 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -126,6 +126,41 @@ describe("slots", () => {
|
|||||||
expect(mockConsoleWarn).toBeCalledTimes(1);
|
expect(mockConsoleWarn).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("simple default slot with params and bound function", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<t t-slot="default" fn.bind="getValue"/>`;
|
||||||
|
state = useState({ value: 123 });
|
||||||
|
getValue() {
|
||||||
|
return this.state.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<Child t-slot-scope="slotScope"><t t-esc="slotScope.fn()"/></Child>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("123");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default slot with params with - in it", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<t t-slot="default" some-value="state.value"/>`;
|
||||||
|
state = useState({ value: 123 });
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<Child t-slot-scope="slotScope"><t t-esc="slotScope['some-value']"/></Child>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("123");
|
||||||
|
});
|
||||||
|
|
||||||
test("fun: two calls to the same slot", async () => {
|
test("fun: two calls to the same slot", async () => {
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
static template = xml`<t t-slot="default"/><t t-slot="default"/>`;
|
static template = xml`<t t-slot="default"/><t t-slot="default"/>`;
|
||||||
@@ -242,6 +277,30 @@ describe("slots", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can define and call slots with bound params", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<t t-slot="abc"/>
|
||||||
|
<t t-esc="props.slots['abc'].getValue()"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static components = { Child };
|
||||||
|
static template = xml`
|
||||||
|
<Child>
|
||||||
|
<t t-set-slot="abc" getValue.bind="getValue">abc</t>
|
||||||
|
</Child>`;
|
||||||
|
state = useState({ value: 444 });
|
||||||
|
getValue() {
|
||||||
|
return this.state.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("abc444");
|
||||||
|
});
|
||||||
|
|
||||||
test("no named slot content => just no children", async () => {
|
test("no named slot content => just no children", async () => {
|
||||||
class Dialog extends Component {
|
class Dialog extends Component {
|
||||||
static template = xml`<span><t t-slot="header"/></span>`;
|
static template = xml`<span><t t-slot="header"/></span>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user