Chào mừng đến với Cafedev! Trong bài viết này, chúng ta sẽ khám phá về Slots trong Vue.js. Slots là một tính năng mạnh mẽ giúp chúng ta tạo ra các thành phần linh hoạt và dễ bảo trì. Hãy cùng đi sâu vào Slots và tìm hiểu cách chúng ta có thể sử dụng chúng để tạo ra các ứng dụng Vue.js linh hoạt và mạnh mẽ hơn!
Trang này giả định bạn đã đọc qua các Khái niệm cơ bản về Components. Hãy đọc trước nếu bạn mới làm quen với components.
Nội dung chính
1. Nội dung Slot và Điểm đặt
Chúng ta đã biết rằng components 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 template thì sao? Trong một số trường hợp, chúng ta có thể muốn truyền một đoạn template tới một component con, và để cho component con render đoạn template đó trong template của nó.
Ví dụ, chúng ta có thể có một component <FancyButton> hỗ trợ việc sử dụng như sau:
<FancyButton>
Click me! <!-- slot content -->
</FancyButton>
Template của <FancyButton> trông như sau:
<button class="fancy-btn">
<slot></slot> <!-- slot outlet -->
</button>
Phần tử <slot> là một điểm đặt slot chỉ ra nơi mà nội dung slot do parent cung cấp nên được render:
Và DOM cuối cùng được render:
<button class="fancy-btn">Click me!</button>
Với slots, <FancyButton> có trách nhiệm hiển thị <button> bên ngoài (và kiểu dáng đặc biệt 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ề slots là bằng cách so sánh chúng với các hàm JavaScript:
// 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>`
}
Nội dung slot không chỉ giới hạn trong văn bản. Nó có thể là bất kỳ nội dung template 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 components khác:
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
Bằng cách sử dụng slots, của chúng ta linh hoạt và có thể tái sử dụng hơn. Bây giờ, chúng ta có thể sử dụng nó ở nhiều nơi khác nhau với nội dung bên trong khác nhau, nhưng vẫn giữ nguyên phong cách đẹp mắt.
Cơ chế slot của Vue components được lấy cảm hứng từ phần tử <slot> của Web Component cơ bản, nhưng với các khả năng bổ sung mà chúng ta sẽ thấy sau này.
2. Phạm vi Render
Nội dung slot có quyền truy cập vào phạm vi dữ liệu của component cha, vì nó được định nghĩa trong phần cha. Ví dụ:
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
Ở đây cả hai sự nội suy {{ message }}
sẽ render cùng nội dung.
Nội dung slot không có quyền truy cập vào dữ liệu của component con. Biểu thức trong template Vue chỉ có thể truy cập vào phạm vi nó được định nghĩa, tương tự như phạm vi léxical của JavaScript. Nói cách khác:
Biểu thức trong template cha chỉ có quyền truy cập vào phạm vi của cha; biểu thức trong template con chỉ có quyền truy cập vào phạm vi của con.
3. Nội dung dự phòng
Có những trường hợp khi việc chỉ định nội dung dự phòng (tức là mặc định) cho một slot rất hữu ích, chỉ được render khi không có nội dung được cung cấp. Ví dụ, trong một <SubmitButton> component :
<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 thẻ <button> nếu thành phần cha không cung cấp bất kỳ nội dung slot nào. Để làm cho “Submit” trở thành nội dung dự phòng, chúng ta có thể đặt nó giữa các thẻ <slot>:
<button type="submit">
<slot>
Submit <!-- fallback content -->
</slot>
</button>
Bây giờ khi chúng ta sử dụng <SubmitButton> trong một component cha, không cung cấp nội dung cho slot:
<SubmitButton />
Điều này sẽ render nội dung dự phòng, “Submit”:
<button type="submit">Submit</button>
Nhưng nếu chúng ta cung cấp nội dung:
<SubmitButton>Save</SubmitButton>
Sau đó nội dung được cung cấp sẽ được render thay vì:
<button type="submit">Save</button>
4. Slots Đặt Tên
Có những lúc khi có nhiều slot outlets trong một component duy nhất là hữu ích. Ví dụ, trong một component <BaseLayout> với template sau:
<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>
Đối với những trường hợp này, phần tử <slot> có một thuộc tính đặc biệt, name
, có thể được sử dụng để gán một ID duy nhất cho các slot khác nhau để bạn có thể xác định nơi nội dung sẽ được render:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
Một <slot> outlet không có name
mặc định có tên là “default”.
Trong một component cha sử dụng <BaseLayout>, chúng ta cần một cách để truyền nhiều đoạn nội dung slot, mỗi đoạn nhắm mục tiêu tới một slot outlet khác nhau. Đây là nơi mà slots đặt tên xuất hiện.
Để chuyển một slot có tên, chúng ta cần sử dụng một phần tử <template> với chỉ thị v-slot, và sau đó truyền tên của slot như một đối số cho v-slot:
<BaseLayout>
<template v-slot:header>
<!-- content for the header slot -->
</BaseLayout>
v-slot có một viết tắt riêng #, vì vậy <template v-slot:header> có thể được rút gọn thành chỉ <template #header>. Hãy tưởng tượng rằng đó là “hiển thị đoạn mẫu này trong slot ‘header’ của thành phần con”
Dưới đây là đoạn mã truyền nội dung cho tất cả ba slots vào sử dụng cú pháp viết tắt:
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</BaseLayout>
Khi một thành phần chấp nhận cả slot mặc định và slot có tên, tất cả các nút cấp cao không phải là <template> được xử lý mặc định như nội dung cho slot mặc định. Vì vậy, đoạn mã trên cũng có thể được viết như sau:
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
<!-- 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>
</BaseLayout>
Bây giờ mọi thứ bên trong các phần tử <template> sẽ được chuyển đến các slot tương ứng. HTML cuối cùng được hiển thị sẽ là:
<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>
Thử nghiệm trên Playground
Một lần nữa, việc này có thể giúp bạn hiểu rõ hơn về các slots đặt tên bằng việc dùng tương tự như hàm JavaScript:
// 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. Slots Điều Kiện
Đôi khi bạn muốn render một cái gì đó dựa vào việc một slot có tồn tại hay không.
Bạn có thể sử dụng thuộc tính $slots kết hợp với một v-if để làm được điều này.
Trong ví dụ dưới đây, chúng ta định nghĩa một component Card với hai slots điều kiện: header
và footer
. Khi header / footer có mặt, chúng ta muốn bọc chúng để cung cấp thêm kiểu dáng:
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-content">
<slot />
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
6. Tên Slots Động
Đối số dynamic directive cũng hoạt động trên v-slot
, cho phép định nghĩa tên slots động:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
<!-- with shorthand -->
<template #[dynamicSlotName]>
...
</base-layout>
Lưu ý biểu thức này phải tuân thủ ràng buộc cú pháp của đối số dynamic directive.
7. Slots Scoped
Như đã thảo luận trong Phạm vi Render, nội dung slot không có quyền truy cập vào trạng thái trong component con.
Tuy nhiên, có những trường hợp nơi mà nó có thể hữu ích nếu nội dung của slot có thể sử dụng dữ liệu từ cả phạm vi của component cha và component con. Để làm được điều đó, chúng ta cần một cách để component con truyền dữ liệu vào slot khi render nó.
Trong thực tế, chúng ta có thể làm đúng như vậy – chúng ta có thể truyền các thuộc tính vào một slot outlet giống như việc truyền props vào một component:
<!-- <MyComponent> template -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
Nhận các props của slot có chút khác biệt khi sử dụng một default slot duy nhất so với việc sử dụng named slots. Chúng ta sẽ hiển thị cách nhận props bằng cách sử dụng một default slot đầu tiên, bằng cách sử dụng v-slot
trực tiếp trên thẻ component con:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
Thử nghiệm trên Playground
Các props được truyền vào slot bởi component con có sẵn như giá trị của chỉ thị v-slot
tương ứng, có thể được truy cập bởi biểu thức bên trong slot.
Bạn có thể nghĩ về một scoped slot như một hàm được truyền vào component con. Component con sau đó gọi nó, truyền props như là đối số:
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>`
}
Trong thực tế, điều này rất gần với cách scoped slots được biên dịch, và cách bạn sẽ sử dụng scoped slots trong các hàm render thủ công.
Lưu ý cách v-slot="slotProps"
khớp với chữ ký hàm của slot. Giống như với các đối số hàm, chúng ta có thể sử dụng destructuring trong v-slot
:
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
7.1 Named Scoped Slots
Named scoped slots hoạt động tương tự – các props của slot có thể được truy cập như là giá trị của chỉ thị v-slot
: v-slot:name="slotProps"
. Khi sử dụng cách viết tắt, nó sẽ trông như sau:
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
<template #default="defaultProps">
{{ defaultProps }}
<template #footer="footerProps">
{{ footerProps }}
</MyComponent>
Truyền props cho một named slot:
<slot name="header" message="hello"></slot>
Lưu ý rằng name
của một slot sẽ không được bao gồm trong các props vì nó được dành trước – vì vậy kết quả headerProps
sẽ là { message: 'hello' }
.
Nếu bạn đang kết hợp các slot có tên với slot mặc định được phạm vi, bạn cần sử dụng một thẻ <template> rõ ràng cho slot 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 lỗi biên dịch. Điều này nhằm tránh bất kỳ sự mơ hồ nào về phạm vi của các props của slot mặc định. Ví dụ:
<!-- This template won't compile -->
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- message belongs to the default slot, and is not available here -->
<p>{{ message }}</p>
</MyComponent>
Sử dụng một thẻ <template> rõ ràng cho slot mặc định giúp làm rõ ràng rằng thuộc tính message không khả dụng trong slot khác:
<MyComponent>
<!-- Use explicit default slot -->
<template #default="{ message }">
<p>{{ message }}</p>
<template #footer>
<p>Here's some contact info</p>
</MyComponent>
7.2 Ví dụ Danh sách Phong Cách
Bạn có thể tự hỏi điều gì sẽ là một trường hợp sử dụng tốt cho scoped slots. Dưới đây <FancyList> là một ví dụ: hãy tưởng tượng một component 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 tiên tiến như phân trang hoặc cuộn vô tận. Tuy nhiên, chúng ta muốn nó linh hoạt với cách mỗi mục trông như thế nào và để việc trình bày từng mục đều được chuyển sang component cha tiêu thụ nó. Vì vậy, việc sử dụng mong muốn có thể trông như thế này:
<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>
</FancyList>
Bên trong <FancyList>, chúng ta 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 ý chúng ta đang sử dụng v-bind
để truyền một object như là props của slot):
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
7.3 Các Components Không Render
Trường hợp sử dụng <FancyList> mà chúng ta đã thảo luận ở trê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 ủy quyền một phần của đầu ra trực quan cho component tiêu thụ thông qua scoped slots.
Nếu chúng ta đẩy khái niệm này một chút xa hơn, chúng ta có thể tạo ra các components chỉ đóng gói logic và không hiển thị bất cứ điều gì bởi chính họ – đầu ra trực quan hoàn toàn được ủy quyền cho component tiêu thụ thông qua scoped slots. Chúng ta gọi loại component này là Components Không Render.
Một ví dụ về component không render có thể là một component mà đóng gói logic theo dõi vị trí chuột hiện tại:
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
Thử nghiệm trên Playground
Mặc dù là một mẫu thú vị, hầu hết những gì có thể đạt được với Components Không Render có thể đạt được một cách hiệu quả hơn với Composition API, mà không gây ra chi phí của việc lồng thêm các component. Sau này, chúng ta sẽ xem làm thế nào chúng ta có thể triển khai cùng chức năng theo dõi chuột như là một Composable.
Tuy vậy, scoped slots vẫn hữu ích trong các trường hợp chúng ta cần đóng gói logic và tổng hợp đầu ra trực quan, như trong ví dụ của <FancyList>.
Trên đây là một cái nhìn tổng quan về Slots trong Vue.js. Hy vọng rằng sau bài viết này, bạn đã có cái nhìn rõ ràng hơn về cách sử dụng và tirao sáng Slots trong ứng dụng Vue của mình. Đừng ngần ngại thử nghiệm và áp dụng kiến thức đã học vào dự án của bạn. Hãy tiếp tục theo dõi Cafedev để cập nhật thêm nhiều thông tin hữu ích về lập trình và công nghệ!
Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Vuejs từ cơ bản tới nâng cao
Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!