🦉 插槽(Slots)🦉
概述
Owl 是一个基于模板的组件系统,因此我们需要一种方式来编写通用组件。 例如,一个通用的 Navbar
(导航栏)组件需要支持自定义的内容。由于这些内容只有使用组件的人才知道,因此理想的做法是在使用组件的地方传入内容:
<div>
<Navbar>
<span>Hello Owl</span>
</Navbar>
</div>
这正是插槽(slot)的作用! 在上面的例子中,Navbar
组件的使用者定义了内容(即默认插槽中的 <span>Hello Owl</span>
)。 而 Navbar
组件可以使用 t-slot
指令将该内容插入自身模板中的合适位置。
需要特别注意的是:插槽的内容是在父组件的上下文中渲染的,因此它可以访问父组件中的变量与方法。
例如,Navbar
组件可以这样定义:
<div class="navbar">
<t t-slot="default"/>
<ul>
<!-- 其他导航内容 -->
</ul>
</div>
命名插槽(Named Slots)
默认插槽很有用,但有时我们需要多个插槽。这时可以使用“命名插槽”。
假设我们实现一个 InfoBox
组件,它有标题和内容两个部分:
<div class="info-box">
<div class="info-box-title">
<t t-slot="title"/>
<span class="info-box-close-button" t-on-click="close">X</span>
</div>
<div class="info-box-content">
<t t-slot="content"/>
</div>
</div>
使用时可通过 t-set-slot
指定不同的插槽内容:
<InfoBox>
<t t-set-slot="title">
自定义标题,可以包含 HTML
</t>
<t t-set-slot="content">
<!-- 自定义内容,支持 HTML、事件等 -->
</t>
</InfoBox>
渲染上下文
插槽内容在其定义位置的上下文中渲染,而不是插入的位置。 这保证了事件处理函数能绑定在正确的组件上(通常是插槽内容的父组件)。
默认插槽
如果插入的内容没有使用命名插槽,它会自动归为 default
插槽:
<div t-name="Parent">
<Child>
<span>内容</span>
</Child>
</div>
<div t-name="Child">
<t t-slot="default"/>
</div>
你可以同时使用默认插槽和命名插槽:
<div>
<Child>
默认插槽内容
<t t-set-slot="footer">
页脚内容
</t>
</Child>
</div>
默认内容
如果父组件没有定义插槽内容,则子组件可以定义插槽的默认内容:
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">默认内容</t>
</span>
<!-- 渲染结果:<div><span>默认内容</span></div> -->
动态插槽(Dynamic Slots)
t-slot
指令可以使用动态表达式(字符串插值):
<t t-slot="{{current}}"/>
这会计算 current
的值,并将相应的插槽内容插入到这个位置。
插槽与 props 的关系
插槽本质上类似于 props
:它们都是向子组件传递信息的一种方式。 为了实现插槽的组合与传递,Owl 提供了一个特殊的 props.slots
属性,包含所有插槽信息:
{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }
因此,一个组件可以将自己的插槽传递给子组件:
<Child slots="props.slots"/>
插槽参数(Slot Params)
对于高级用例,有时需要传递额外的信息给插槽。可以在 t-set-slot
中添加键值对作为参数,子组件可通过 props.slots
读取这些值。
例如,一个分页组件 Notebook
:
class Notebook extends Component {
static template = xml`
<div class="notebook">
<div class="tabs">
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab_index">
<t t-esc="props.slots[tab].title"/>
</span>
</t>
</div>
<div class="page">
<t t-slot="{{currentSlot}}"/>
</div>
</div>`;
setup() {
this.state = useState({ activeTab: 0 });
this.tabNames = Object.keys(this.props.slots);
}
get currentSlot() {
return this.tabNames[this.state.activeTab];
}
}
使用时传入参数:
<Notebook>
<t t-set-slot="page1" title.translate="页面 1">
<div>这是第 1 页</div>
</t>
<t t-set-slot="page2" title.translate="页面 2" hidden="somevalue">
<div>这是第 2 页</div>
</t>
</Notebook>
参数支持 .translate
(翻译)和 .bind
(函数绑定)等语法。
插槽作用域(Slot Scopes)
有些情况下,插槽内容需要访问子组件提供的信息。这种情况可以使用 t-slot-scope
。
在使用方中:
<MyComponent>
<t t-set-slot="foo" t-slot-scope="scope">
插槽内容:
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</t>
</MyComponent>
而子组件提供数据:
<t t-slot="foo" bool="other_var" num="5"/>
或者:
<t t-slot="foo" t-props="someObject"/>
如果使用的是默认插槽,也可以直接在组件标签上定义作用域变量:
<MyComponent t-slot-scope="scope">
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</MyComponent>
这些作用域变量与普通 props
一样,也支持 .bind
等语法。