Luna Tech

Tutorials For Dummies.

Vue: Slots

2022-03-27


0. Intro

Slots | Vue.js (vuejs.org)

What are Slots?

我们之前学过了 props 的相关内容,知道 components 可以接收 props(JS values of any type),那么我们能不能通过 template(也就是 HTML)来传入一些内容呢?比如要 render text 或者 HTML。

当然可以啦!这就是我们今天要学习的 Slots!

<slot>element 是一个 slot outlet,表示 parent-provided slot content should be rendered here.

这个概念是由 native Web Component <slot> 激发的,但是比原生的更加 powerful。

Benefits

Components are more flexible and reusable.


1. Examples

Example 1

slot diagram

<!-- parent -->
<FancyButton>
  Click me! <!-- slot content -->
</FancyButton>
<!-- FancyButton.vue -->
<button class="fancy-btn">
  <slot></slot> <!-- slot outlet -->
</button>
<!-- final rendered DOM -->
<button class="fancy-btn">
  Click me!
</button>

Explanation

Child (FancyButton) 负责 render outer button 和 outer button 的 style,而 slot 的内容则由 parent component 提供。

Example 2

我们还可以通过把 slot 和 JS function 进行对比来理解这个概念。

child 是直接把 slot content 作为 variable 来 render 的。

// parent component passing slot content
FancyButton('Click me!')

// FancyButton renders slot content in its own template
function FancyButton(slotContent) {
  return (
    `<button class="fancy-btn">
      ${slotContent}
    </button>`
  )
}

Example 3 - render any valid template content with slot

Slot 的内容不仅限于 text,只要是有效的 template content 都可以。比如我们可以 pass in multiple elements,甚至 other components。

<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

2. Render Scope

可以 access parent,不能 access child

slot content 可以 access parent component 的 data scope,因为它是在 parent 里面定义的。

slot content 不能 access child component data。

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

比如上面的例子里面,message 是指向同一个 variable。

原因 - compile time 不同

Everything in the parent template is compiled in parent scope; everything in the child template is compiled in the child scope.


3. Fallback/Default Content

有时候我们可能需要明确定义 slot 的 default content,就是当 parent 没有给任何 content 的时候,render 什么。

Example - without fallback content

<button type="submit">
  <slot></slot>
</button>

Example - with fallback content

<button type="submit">
  <slot>
    Submit <!-- fallback content -->
  </slot>
</button>

4. Named Slots

适用场合

当你在一个 component 里面有多个 slots 的时候,我们可以给每个 slot 一个独特的名字 name attribute 来区分。

一个没有名字的 slot,默认的名字是 default

named slots diagram

Example - multiple contents

<div class="container">
  <header>
    <!-- We want header content here -->
  </header>
  <main>
    <!-- We want main content here -->
  </main>
  <footer>
    <!-- We want footer content here -->
  </footer>
</div>
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Parent - 用 v-slot directive + slot name 来 map

<BaseLayout>
  <template v-slot:header>
    <!-- content for the header slot -->
  </template>
</BaseLayout>

Shorthand - #slotname

v-slot has a dedicated shorthand #, so <template v-slot:header> can be shortened to just <template #header>. Think of it as “render this template fragment in the child component’s ‘header’ slot”.

Example 1 - explicit default slot

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

Example 2 - implicit default slot

当 component 同时有 default slot 和 named slot 的时候,所有 top-level non <template> nodes 都默认是 default slot 的内容。

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- implicit default slot -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

Example 1 和 2 最终的 render 结果是一样的。

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

Example 3 - JS analogy

// passing multiple slot fragments with different names
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> renders them in different places
function BaseLayout(slots) {
  return (
    `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
  )
}

5. Dynamic Slot Names

Dynamic Directive Arguments 也适用于 v-slot,所以我们可以使用动态 slot names:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- with shorthand -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

Do note the expression is subject to the syntax constraints of dynamic directive arguments.


6. Scoped Slots

适用场景

slot 无法 access child component 里面的 state,但是我们想要 access 这些数据。

当我们需要 encapsulate logic and compose visual output 的时候,就可以用到 scoped slot 的概念。

Idea

我们需要 child pass data to a slot when rendering it and parent receives the data from child.

scoped slots diagram

原理

The props passed to the slot by the child are available as the value of the corresponding v-slot directive, which can be accessed by expressions inside the slot.

我们可以把 scoped slot 想象成一个传入 child component 的 function,child 可以 call 这个 function,pass props as arguments.

In fact, this is very close to how scoped slots are compiled, and how you would use scoped slots in manual render functions .

MyComponent({
  // passing the default slot, but as a function
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return (
    `<div>${
      // call the slot function with props!
      slots.default({ text: greetingMessage, count: 1 })
    }</div>`
  )
}

Child passes attributes to slot

我们可以 pass attributes to a slot outlet,就像 pass props to a component 一样。

Example 1 - passing props to a default slot

<!-- <MyComponent> template -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

Example 2 - passing props to a named slot

Note the name of a slot won’t be included in the props because it is reserved - so the resulting headerProps would be { message: 'hello' }.

<slot name="header" message="hello"></slot>

Parent receives slot props

Example 1 - Receiving default slot props

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

Example 2 - Receiving default slot props with destruction

Notice how v-slot="slotProps" matches the slot function signature. Just like with function arguments, we can use destructuring in v-slot.

<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

Example 3 - Receiving named slots props with shorthand

slot props are accessible as the value of the v-slot directive: v-slot:name="slotProps".

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

Complete Example

比如我们希望有 flexible styling,parent 来定义 child 的 style,child handle data logic。

Imagine a <FancyList> component that renders a list of items - it may encapsulate the logic for loading remote data, using the data to display a list, or even advanced features like pagination or infinite scrolling. However, we want it to be flexible with how each item looks and leave the stying of each item to the parent component consuming it.

<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

在 FancyList 里面,我们可以多次 render 同一个 slot with different item data(用 v-bind 来 pass an object as slot props)。

<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

7. Renderless Components

从 scoped slots 这个概念继续深挖,我们可以有一些只负责 logic,不管 render 的 components,visual output is fully delegated to the consumer component with scoped slots.

这种 component 就被称为 Renderless component。

Example - encapsulates the logic of tracking current mouse position

<MouseTracker v-slot="{ x, y }">
  Mouse is at: {{ x }}, {{ y }}
</MouseTracker>

Better solution: Composition API

这种 renderless component pattern 可以通过 composition api 来更加有效地完成,用 composition api 可以避免 overhead of extra component nesting.

我们接下来会学习如何用 Composable 这个概念来完成同样的 mouse tracking functionality。