Skip to content

Slots

Nội dung Khe cắm và Điểm xuất

Chúng ta đã biết rằng các thành phần có thể chấp nhận props, có thể là các giá trị JavaScript của bất kỳ loại nào. Nhưng với nội dung mẫu thì sao? Trong một số trường hợp, chúng ta có thể muốn truyền một đoạn mã mẫu đến một thành phần con và để cho thành phần con hiển thị đoạn mã đó trong mẫu của nó.

Ví dụ, chúng ta có thể có một thành phần <FancyButton> hỗ trợ việc sử dụng như sau:

template
<FancyButton>
  Click me! <!-- nội dung khe cắm -->
</FancyButton>

Mẫu của <FancyButton> trông như thế này:

template
<button class="fancy-btn">
  <slot></slot> <!-- điểm xuất khe cắm -->
</button>

Phần tử <slot> là một điểm xuất khe cắm chỉ định nơi nội dung khe cắm được cung cấp bởi phần tử cha nên được hiển thị.

sơ đồ khe cắm

Và DOM được hiển thị cuối cùng:

html
<button class="fancy-btn">Click me!</button>

Với khe cắm, <FancyButton> chịu trách nhiệm hiển thị phần tử <button> bên ngoài (và kiểu trang trí phức tạp của nó), trong khi nội dung bên trong được cung cấp bởi thành phần cha.

Một cách khác để hiểu về khe cắm là bằng cách so sánh chúng với các hàm JavaScript:

js
// thành phần cha truyền nội dung khe cắm
FancyButton('Click me!')

// FancyButton hiển thị nội dung khe cắm trong mẫu của mình
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
      ${slotContent}
    </button>`
}

Nội dung khe cắm không chỉ giới hạn trong văn bản. Nó có thể là bất kỳ nội dung mẫu hợp lệ nào. Ví dụ, chúng ta có thể truyền vào nhiều phần tử hoặc thậm chí là các thành phần khác:

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

[Thử nghiệm trong Playground](https://play.vuejs.org/#eNp1UmtOwkAQvspQYtCEgrx81EqCJibeoX+W7bRZaHc3+1AI4QyewH8ewvN4Aa/gbgtNIfFf5+vMfI/ZXbCQcvBmMYiCWFPFpAGNxsp5wlkphTLwQjjdPlljBIdMi

RJ6g2EL88O9pnnxjlqU+EpbzS3s0BwPaypH4gqDpSyIQVcBxK3VFQDwXDC6hhJdlZi4zf3fRKwl4aDNtsDHJKCiECqiW8KTYH5c1gEnwnUdJ9rCh/XeM6Z42AgN+sFZAj6+Ux/LOjFaEK2diMz3h0vjNfj/zokuhPFU3lTdfcpShVOZcJ+DZgHs/HxtCrpZlj34eknoOlfC8jSCgnEkKswVSRlyczkZzVLM+9CdjtPJ/RjGswtX3ExvMcuu6mmhUnTruOBYAZKkKeN5BDO5gdG13FRoSVTOeAW2xkLPY3UEdweYWqW9OCkYN6gctq9uXllx2Z09CJ9dJwzBascI7nBYihWDldUGMqEgdTVIq6TQqCEMfUpNSD+fX7/fH+3b7P8AdGP6wA==)

Bằng cách sử dụng khe cắm, <FancyButton> của chúng ta trở nên linh hoạt và có thể tái sử dụng. Chúng ta có thể sử dụng nó ở nhiều nơi với nội dung bên trong khác nhau, nhưng vẫn giữ nguyên kiểu trang trí phức tạp của nó.

Cơ chế khe cắm của thành phần Vue được lấy cảm hứng từ phần tử khe cắm <slot> của Web Component nguyên bản, nhưng có thêm những khả năng bổ sung chúng ta sẽ thấy sau này.

Phạm vi Kết xuất

Nội dung khe cắm có quyền truy cập vào phạm vi dữ liệu của thành phần cha, vì nó được định nghĩa trong thành phần cha. Ví dụ:

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

Ở đây cả hai sự nội suy {{ message }} sẽ hiển thị nội dung giống nhau.

Nội dung khe cắm không có quyền truy cập vào dữ liệu của thành phần con. Biểu thức trong các mẫu Vue chỉ có thể truy cập phạm vi mà nó được định nghĩa trong, nhất quán với phạm vi tĩnh của JavaScript. Nói cách khác:

Biểu thức trong mẫu của cha chỉ có quyền truy cập vào phạm vi của cha; biểu thức trong mẫu của con chỉ có quyền truy cập vào phạm vi của con.

Nội dung Phụ thuộc

Có những trường hợp khi việc chỉ định nội dung phụ thuộc (tức là nội dung mặc định) cho một khe cắm là hữu ích, chỉ được hiển thị khi không có nội dung nào được cung cấp. Ví dụ, trong một thành phần <SubmitButton>:

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

Chúng ta có thể muốn văn bản "Submit" được hiển thị bên trong <button> nếu thành phần cha không cung cấp bất kỳ nội dung khe cắm nào. Để làm cho "Submit" trở thành nội dung phụ thuộc, chúng ta có thể đặt nó giữa các thẻ <slot>:

template
<button type="submit">
  <slot>
    Submit <!-- nội dung phụ thuộc -->
  </slot>
</button>

Bây giờ khi chúng ta sử dụng <SubmitButton> trong một thành phần cha và không cung cấp bất kỳ nội dung nào cho khe cắm:

template
<SubmitButton />

Điều này sẽ hiển thị nội dung phụ thuộc, "Submit":

html
<button type="submit">Submit</button>

Nhưng nếu chúng ta cung cấp nội dung:

template
<SubmitButton>Save</SubmitButton>

Thì nội dung được cung cấp sẽ được hiển thị thay vào đó:

html
<button type="submit">Save</button>

Khe cắm có Tên

Có những trường hợp khi có nhiều khe cắm trong một thành phần duy nhất là hữu ích. Ví dụ, trong một thành phần <BaseLayout> với mẫu sau:

template
<div class="container">
  <header>
    <!-- Chúng ta muốn có nội dung của header ở đây -->
  </header>
  <main>
    <!-- Chúng ta muốn có nội dung chính ở đây -->
  </main>
  <footer>
    <!-- Chúng ta muốn có nội dung của footer ở đây -->
  </footer>
</div>

Đối với những trường hợp như vậy, phần tử <slot> có một thuộc tính đặc biệt là name, có thể được sử dụng để gán một ID duy nhất cho các khe cắm khác nhau để bạn có thể xác định nơi mà nội dung nên được hiển thị:

template
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Một khe cắm <slot> không có name mặc định sẽ có tên là "default".

Trong một thành phần cha sử dụng <BaseLayout>, chúng ta cần một cách để truyền nhiều đoạn nội dung khe cắm, mỗi đoạn dành cho một khe cắm khác nhau. Đây là nơi khe cắm có tên giúp ích.

Để truyền một khe cắm có tên, chúng ta cần sử dụng một phần tử <template> với chỉ thị v-slot, sau đó truyền tên của khe cắm như một đối số cho v-slot:

template
<BaseLayout>
  <template v-slot:header>
    <!-- Nội dung cho khe cắm header -->
  </template>
</BaseLayout>

v-slot có một cú pháp rút gọn là #, vì vậy <template v-slot:header> có thể được rút gọn thành <template #header>. Hãy xem đó như là "hiển thị đoạn mẫu này trong khe cắm 'header' của thành phần con".

named slots diagram

Dưới đây là mã để truyền nội dung cho tất cả ba khe cắm vào <BaseLayout> sử dụng cú pháp rút gọn:

template
<BaseLayout>
  <template #header>
    <h1>Đây có thể là tiêu đề trang</h1>
  </template>

  <template #default>
    <p>Một đoạn văn bản cho nội dung chính.</p>
    <p>Và một đoạn khác.</p>
  </template>

  <template #footer>
    <p>Đây là một số thông tin liên hệ</p>
  </template>
</BaseLayout>

Khi một thành phần chấp nhận cả một khe cắm mặc định và khe cắm có tên, tất cả các nút cấp cao không phải là <template> sẽ tự động được xử lý như là nội dung cho khe cắm mặc định. Vì vậy, đoạn mã trên có thể được viết như sau:

template
<BaseLayout>
  <template #header>
    <h1>Đây có thể là tiêu đề trang</h1>
  </template>

  <!-- Khe cắm mặc định ngầm định -->
  <p>Một đoạn văn bản cho nội dung chính.</p>
  <p>Và một đoạn khác.</p>

  <template #

footer>
    <p>Đây là một số thông tin liên hệ</p>
  </template>
</BaseLayout>

Bây giờ mọi thứ bên trong các phần tử <template> sẽ được chuyển đến các khe cắm tương ứng. HTML cuối cùng sẽ là:

html
<div class="container">
  <header>
    <h1>Đây có thể là tiêu đề trang</h1>
  </header>
  <main>
    <p>Một đoạn văn bản cho nội dung chính.</p>
    <p>Và một đoạn khác.</p>
  </main>
  <footer>
    <p>Đây là một số thông tin liên hệ</p>
  </footer>
</div>

Một lần nữa, việc hiểu khe cắm có tên có thể dễ dàng hơn thông qua phân biệt bằng phương pháp giống như hàm JavaScript:

js
// truyền nhiều đoạn nội dung cho các khe cắm khác nhau
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> hiển thị chúng ở các vị trí khác nhau
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

Tên Khe cắm Động

Đối số động của chỉ thị cũng hoạt động trên v-slot, cho phép định nghĩa tên khe cắm động:

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

  <!-- với cú pháp viết tắt -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

Lưu ý rằng biểu thức này phải tuân theo ràng buộc cú pháp của đối số động của chỉ thị.

Khe cắm Động Phạm vi

Như đã thảo luận trong Phạm vi Kích thước, nội dung khe cắm không có quyền truy cập vào trạng thái trong thành phần con.

Tuy nhiên, có những trường hợp nơi nó có thể hữu ích nếu nội dung của khe cắm có thể sử dụng dữ liệu từ cả phạm vi của cha và phạm vi của con. Để đạt được điều đó, chúng ta cần một cách để con truyền dữ liệu cho một khe cắm khi hiển thị nó.

Thực sự, chúng ta có thể làm chính xác điều đó - chúng ta có thể truyền thuộc tính cho một khe cắm giống như truyền props cho một thành phần:

template
<!-- Mẫu của <MyComponent> -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

Việc nhận các props của khe cắm có chút khác biệt khi sử dụng một khe cắm mặc định đơn so với việc sử dụng các khe cắm được đặt tên. Chúng tôi sẽ chỉ cách nhận các props bằng cách sử dụng một khe cắm mặc định đầu tiên, bằng cách sử dụng v-slot trực tiếp trên thẻ thành phần con:

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

sơ đồ khe cắm phạm vi

Các props được truyền vào khe cắm bởi thành phần con có sẵn như giá trị của chỉ thị v-slot tương ứng, có thể được truy cập thông qua các biểu thức bên trong khe cắm.

Bạn có thể xem một khe cắm phạm vi như là một hàm được truyền vào thành phần

con. Sau đó, thành phần con gọi nó, truyền props như là đối số:

js
MyComponent({
  // Truyền khe cắm mặc định, nhưng như một hàm
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'xin chào'
  return `<div>${
    // Gọi hàm khe cắm với props!
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`
}

Thực sự, điều này rất gần với cách khe cắm phạm vi được biên dịch và cách bạn sẽ sử dụng khe cắm phạm vi trong các hàm biên dịch thủ công.

Lưu ý là v-slot="slotProps" khớp với chữ ký hàm của khe cắm. Giống như với đối số của hàm, chúng ta có thể sử dụng phá hủy trong v-slot:

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

Khe cắm Phạm vi Đặt Tên

Khe cắm phạm vi đặt tên hoạt động tương tự - props của khe cắm có sẵn như giá trị của chỉ thị v-slot: v-slot:name="slotProps". Khi sử dụng cú pháp viết tắt, nó trông như sau:

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

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

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

Truyền props cho một khe cắm được đặt tên:

template
<slot name="header" message="xin chào"></slot>

Lưu ý rằng name của một khe cắm không sẽ không được bao gồm trong props vì nó được dành trước - vì vậy headerProps kết quả sẽ là { message: 'xin chào' }.

Nếu bạn kết hợp khe cắm được đặt tên với khe cắm mặc định của phạm vi, bạn cần sử dụng một thẻ <template> rõ ràng cho khe cắm mặc định. Cố gắng đặt chỉ thị v-slot trực tiếp trên thành phần sẽ dẫn đến một lỗi biên dịch. Điều này nhằm tránh mọi sự mơ hồ về phạm vi của props của khe cắm mặc định. Ví dụ:

template
<!-- Mẫu này sẽ không biên dịch -->
<template>
  <MyComponent v-slot="{ message }">
    <p>{{ message }}</p>
    <template #footer>
      <!-- message thuộc về khe cắm mặc định và không có sẵn ở đây -->
      <p>{{ message }}</p>
    </template>
  </MyComponent>
</template>

Sử dụng một thẻ <template> rõ ràng cho khe cắm mặc định giúp làm cho rõ ràng rằng prop message không có sẵn trong khe cắm khác:

template
<template>
  <MyComponent>
    <!-- Sử dụng khe cắm mặc định rõ ràng -->
    <template #default="{ message }">
      <p>{{ message }}</p>
    </template>

    <template #footer>
      <p>Đây là một số thông tin liên hệ</p>
    </template>
  </MyComponent>
</template>

Ví dụ Danh sách Phức tạp

Có thể bạn đang tự hỏi làm thế nào để sử dụng khe cắm phạm vi. Dưới đây là một ví dụ: hãy tưởng tượng một thành phần <FancyList> mà hiển thị một danh sách các mục - nó có thể đóng gói logic để tải dữ liệu từ xa, sử dụng dữ liệu để hiển thị một danh sách, hoặc thậm chí các tính năng nâng cao như phân trang hoặc cuộn vô tận. Tuy nhiên, chúng tôi muốn nó linh hoạt với cách mỗi mục trông như thế nào và để lại việc thiết kế cho mỗi mục cho thành phần cha tiêu thụ nó. Vì vậy, cách sử dụng mong muốn có thể trông như thế này:

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

Bên trong <FancyList>, chúng tôi có thể hiển thị cùng một <slot> nhiều lần với dữ liệu mục khác nhau (lưu ý rằng chúng tôi sử dụng v-bind để truyền một đối tượng như props khe cắm):

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

Các Thành Phần Không Render

Trong trường hợp sử dụng <FancyList> mà chúng ta đã thảo luận ở trên, nó đóng gói cả logic có thể tái sử dụng (lấy dữ liệu, phân trang, v.v.) và đầu ra trực quan, trong khi một phần của đầu ra trực quan được chuyển giao cho thành phần tiêu thụ thông qua khe cắm phạm vi.

Nếu chúng ta đẩy khái niệm này một chút, chúng ta có thể tạo ra các thành phần chỉ đóng gói logic mà không hiển thị bất cứ thứ gì một cách độc lập - đầu ra trực quan hoàn toàn được chuyển giao cho thành phần tiêu thụ thông qua khe cắm phạm vi. Chúng tôi gọi loại thành phần này là Thành Phần Không Render.

Một ví dụ về thành phần không render có thể là một thành phần mà đóng gói logic để theo dõi vị trí chuột hiện tại:

template
<MouseTracker v-slot="{ x, y }">
  Chuột đang ở: {{ x }}, {{ y }}
</MouseTracker>

Mặc dù là một mô hình thú vị, nhưng hầu hết những gì có thể đạt được với Thành Phần Không Render có thể đạt được một cách hiệu quả hơn với API Hợp thành, mà không gây ra chi phí của việc nhúng thêm thành phần. Sau này, chúng ta sẽ thấy cách chúng ta có thể thực hiện cùng một chức năng theo dõi chuột dưới dạng Hợp thành.

Tuy nhiên, khe cắm phạm vi vẫn hữu ích trong những trường hợp chúng ta cần đóng gói logic tạo ra đầu ra trực quan, như trong ví dụ của <FancyList>.

Slots has loaded