Skip to content

🦉 插槽(Slots)🦉

概述

Owl 是一个基于模板的组件系统,因此我们需要一种方式来编写通用组件。 例如,一个通用的 Navbar(导航栏)组件需要支持自定义的内容。由于这些内容只有使用组件的人才知道,因此理想的做法是在使用组件的地方传入内容:

xml
<div>
  <Navbar>
    <span>Hello Owl</span>
  </Navbar>
</div>

这正是插槽(slot)的作用! 在上面的例子中,Navbar 组件的使用者定义了内容(即默认插槽中的 <span>Hello Owl</span>)。 而 Navbar 组件可以使用 t-slot 指令将该内容插入自身模板中的合适位置。

需要特别注意的是:插槽的内容是在父组件的上下文中渲染的,因此它可以访问父组件中的变量与方法。

例如,Navbar 组件可以这样定义:

xml
<div class="navbar">
  <t t-slot="default"/>
  <ul>
    <!-- 其他导航内容 -->
  </ul>
</div>

命名插槽(Named Slots)

默认插槽很有用,但有时我们需要多个插槽。这时可以使用“命名插槽”。

假设我们实现一个 InfoBox 组件,它有标题和内容两个部分:

xml
<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 指定不同的插槽内容:

xml
<InfoBox>
  <t t-set-slot="title">
    自定义标题,可以包含 HTML
  </t>
  <t t-set-slot="content">
    <!-- 自定义内容,支持 HTML、事件等 -->
  </t>
</InfoBox>

渲染上下文

插槽内容在其定义位置的上下文中渲染,而不是插入的位置。 这保证了事件处理函数能绑定在正确的组件上(通常是插槽内容的父组件)。


默认插槽

如果插入的内容没有使用命名插槽,它会自动归为 default 插槽:

xml
<div t-name="Parent">
  <Child>
    <span>内容</span>
  </Child>
</div>

<div t-name="Child">
  <t t-slot="default"/>
</div>

你可以同时使用默认插槽和命名插槽:

xml
<div>
  <Child>
    默认插槽内容
    <t t-set-slot="footer">
      页脚内容
    </t>
  </Child>
</div>

默认内容

如果父组件没有定义插槽内容,则子组件可以定义插槽的默认内容:

xml
<div t-name="Parent">
  <Child/>
</div>

<span t-name="Child">
  <t t-slot="default">默认内容</t>
</span>

<!-- 渲染结果:<div><span>默认内容</span></div> -->

动态插槽(Dynamic Slots)

t-slot 指令可以使用动态表达式(字符串插值):

xml
<t t-slot="{{current}}"/>

这会计算 current 的值,并将相应的插槽内容插入到这个位置。


插槽与 props 的关系

插槽本质上类似于 props:它们都是向子组件传递信息的一种方式。 为了实现插槽的组合与传递,Owl 提供了一个特殊的 props.slots 属性,包含所有插槽信息:

js
{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }

因此,一个组件可以将自己的插槽传递给子组件:

xml
<Child slots="props.slots"/>

插槽参数(Slot Params)

对于高级用例,有时需要传递额外的信息给插槽。可以在 t-set-slot 中添加键值对作为参数,子组件可通过 props.slots 读取这些值。

例如,一个分页组件 Notebook

js
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];
  }
}

使用时传入参数:

xml
<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

在使用方中:

xml
<MyComponent>
  <t t-set-slot="foo" t-slot-scope="scope">
    插槽内容:
    <t t-esc="scope.bool"/>
    <t t-esc="scope.num"/>
  </t>
</MyComponent>

而子组件提供数据:

xml
<t t-slot="foo" bool="other_var" num="5"/>

或者:

xml
<t t-slot="foo" t-props="someObject"/>

如果使用的是默认插槽,也可以直接在组件标签上定义作用域变量:

xml
<MyComponent t-slot-scope="scope">
  <t t-esc="scope.bool"/>
  <t t-esc="scope.num"/>
</MyComponent>

这些作用域变量与普通 props 一样,也支持 .bind 等语法。

本站内容仅供学习与参考