Using templates and slots
Bài viết này giải thích cách bạn có thể dùng các phần tử <template> và <slot> để tạo một template linh hoạt, rồi dùng nó để điền vào shadow DOM của một web component.
Sự thật về template
Khi bạn phải dùng đi dùng lại cùng một cấu trúc đánh dấu trên một trang web, việc dùng một dạng template nào đó sẽ hợp lý hơn là lặp lại cấu trúc ấy hết lần này đến lần khác.
Điều này vốn đã khả thi trước đây, nhưng được HTML <template> làm cho dễ hơn rất nhiều.
Phần tử này và nội dung của nó không được render trong DOM, nhưng vẫn có thể được tham chiếu bằng JavaScript.
Hãy xem một ví dụ nhanh đơn giản:
<template id="custom-paragraph">
<p>My paragraph</p>
</template>
Nó sẽ không xuất hiện trên trang cho đến khi bạn lấy tham chiếu tới nó bằng JavaScript rồi append nó vào DOM, bằng cách như sau:
let template = document.getElementById("custom-paragraph");
let templateContent = template.content;
document.body.appendChild(templateContent);
Dù đơn giản, bạn đã có thể bắt đầu thấy nó hữu ích như thế nào.
Dùng template với web component
Template tự nó đã hữu ích, nhưng sẽ còn tốt hơn khi kết hợp với web component.
Hãy định nghĩa một web component dùng template của chúng ta làm nội dung cho shadow DOM của nó.
Chúng ta cũng sẽ gọi nó là <my-paragraph>:
customElements.define(
"my-paragraph",
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById("custom-paragraph");
let templateContent = template.content;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(document.importNode(templateContent, true));
}
},
);
Điểm quan trọng cần lưu ý ở đây là chúng ta append một bản sao của nội dung template vào shadow root, được tạo bằng phương thức Document.importNode().
Và vì chúng ta đang append nội dung của nó vào một shadow DOM, nên có thể đưa một ít thông tin tạo style bên trong template trong một phần tử <style>, rồi phần này sẽ được đóng gói bên trong custom element.
Điều này sẽ không hoạt động nếu chúng ta chỉ append nó vào DOM chuẩn.
Ví dụ:
<template id="custom-paragraph">
<style>
p {
color: white;
background-color: #666666;
padding: 5px;
}
</style>
<p>My paragraph</p>
</template>
Bây giờ chúng ta có thể dùng nó chỉ bằng cách thêm nó vào tài liệu HTML:
<my-paragraph></my-paragraph>
Tăng tính linh hoạt với slot
Đến đây thì ổn, nhưng phần tử vẫn chưa thật linh hoạt.
Chúng ta chỉ có thể hiển thị một mẩu văn bản bên trong nó, nghĩa là hiện tại nó còn ít hữu ích hơn cả một đoạn văn bình thường! Ta có thể làm cho nó hiển thị các đoạn text khác nhau trong mỗi thể hiện phần tử theo một cách khai báo đẹp đẽ bằng cách dùng phần tử <slot>.
Các slot được nhận diện bằng thuộc tính name, và cho phép bạn định nghĩa các chỗ giữ trong template mà có thể được lấp bằng bất kỳ fragment đánh dấu nào bạn muốn khi phần tử được dùng trong markup.
Vì vậy, nếu muốn thêm một slot vào ví dụ đơn giản của chúng ta, ta có thể cập nhật phần tử paragraph trong template như sau:
<p><slot name="my-text">My default text</slot></p>
Nếu nội dung của slot chưa được định nghĩa khi phần tử được đưa vào markup, hoặc nếu trình duyệt không hỗ trợ slot, <my-paragraph> chỉ chứa nội dung dự phòng "My default text".
Để định nghĩa nội dung của slot, chúng ta bao gồm một cấu trúc HTML bên trong phần tử <my-paragraph> với thuộc tính slot có giá trị bằng tên của slot mà ta muốn nó lấp vào. Như trước, đó có thể là bất kỳ thứ gì bạn thích, ví dụ:
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
</my-paragraph>
hoặc
<my-paragraph>
<ul slot="my-text">
<li>Let's have some different text!</li>
<li>In a list!</li>
</ul>
</my-paragraph>
Note: Các nút có thể được chèn vào slot được gọi là nút Slottable; khi một nút đã được chèn vào một slot, nó được gọi là slotted.
Và vậy là xong cho ví dụ đơn giản của chúng ta. Nếu bạn muốn thử thêm, bạn có thể tìm nó trên GitHub (và xem bản chạy trực tiếp nữa).
Thuộc tính name nên là duy nhất trong mỗi shadow root: nếu bạn có hai slot cùng tên, tất cả phần tử có thuộc tính slot khớp sẽ được gán cho slot đầu tiên có tên đó. Nhưng thuộc tính slot thì không cần duy nhất: một <slot> có thể được lấp bởi nhiều phần tử cùng có thuộc tính slot khớp.
Các thuộc tính name và slot đều mặc định là chuỗi rỗng, vì vậy các phần tử không có thuộc tính slot sẽ được gán vào <slot> không có thuộc tính name (slot không đặt tên, hay slot mặc định). Dưới đây là ví dụ:
<template id="custom-paragraph">
<style>
p {
color: white;
background-color: #666666;
padding: 5px;
}
</style>
<p>
<slot name="my-text">My default text</slot>
<slot></slot>
</p>
</template>
Bạn có thể dùng nó như sau:
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
<span>This will go into the unnamed slot</span>
<span>This will also go into the unnamed slot</span>
</my-paragraph>
Trong ví dụ này:
- Nội dung với
slot="my-text"đi vào slot có tên. - Tất cả nội dung khác tự động đi vào slot không đặt tên.
Một ví dụ chi tiết hơn
Để kết thúc bài viết, hãy xem một ví dụ ít đơn giản hơn một chút.
Chuỗi snippet mã sau cho thấy cách dùng <slot> cùng với <template> và một ít JavaScript để:
- tạo một phần tử
<element-details>với named slots trong shadow root của nó - thiết kế phần tử
<element-details>sao cho khi dùng trong tài liệu, nó được render từ việc ghép nội dung của chính phần tử với nội dung từ shadow root của nó - tức là, các phần nội dung của phần tử được dùng để điền vào named slots trong shadow root của nó
Lưu ý rằng về mặt kỹ thuật, có thể dùng phần tử <slot> mà không cần phần tử <template>, ví dụ bên trong một phần tử <div> thông thường, và vẫn tận dụng được tính năng chỗ giữ của <slot> cho nội dung Shadow DOM, và làm như vậy có thể tránh được chút bất tiện là trước tiên phải truy cập thuộc tính content của template rồi clone nó.
Tuy nhiên, nhìn chung sẽ thực tế hơn khi đặt slot bên trong phần tử <template>, vì bạn thường không cần định nghĩa một pattern dựa trên một phần tử đã được render.
Ngoài ra, ngay cả khi nó chưa được render, mục đích của container như một template sẽ rõ nghĩa hơn về mặt ngữ nghĩa khi dùng <template>. Thêm nữa, <template> có thể được thêm trực tiếp các mục bên trong, như <td>, mà sẽ biến mất nếu thêm vào <div>.
Note: Bạn có thể tìm ví dụ hoàn chỉnh này tại element-details (và xem bản chạy trực tiếp nữa).
Tạo một template có một số slot
Trước hết, chúng ta dùng phần tử <slot> bên trong một phần tử <template> để tạo một "element-details-template" mới là một document fragment chứa một số named slots:
<template id="element-details-template">
<style>
details {
font-family: "Open Sans Light", "Helvetica", "Arial";
}
.name {
font-weight: bold;
color: #217ac0;
font-size: 120%;
}
h4 {
margin: 10px 0 -8px 0;
}
h4 span {
background: #217ac0;
padding: 2px 6px;
}
h4 span {
border: 1px solid #cee9f9;
border-radius: 4px;
}
h4 span {
color: white;
}
.attributes {
margin-left: 22px;
font-size: 90%;
}
.attributes p {
margin-left: 16px;
font-style: italic;
}
</style>
<details>
<summary>
<span>
<code class="name"
><<slot name="element-name">NEED NAME</slot>></code
>
<span class="desc"
><slot name="description">NEED DESCRIPTION</slot></span
>
</span>
</summary>
<div class="attributes">
<h4><span>Attributes</span></h4>
<slot name="attributes"><p>None</p></slot>
</div>
</details>
<hr />
</template>
Phần tử <template> này có vài đặc điểm:
-
<template>có một phần tử<style>với một bộ style CSS chỉ được giới hạn trong document fragment mà<template>tạo ra. Các style này được giới hạn theo cách này vì fragment đó sẽ được chèn vào một shadow root. -
<template>dùng<slot>và thuộc tínhnamecủa nó để tạo ba named slots:<slot name="element-name"><slot name="description"><slot name="attributes">
-
<template>bọc các named slots trong một phần tử<details>.
Tạo phần tử mới <element-details> từ <template>
Tiếp theo, hãy tạo một custom element mới tên là <element-details> và dùng Element.attachShadow để gắn vào nó, với tư cách là shadow root, document fragment mà chúng ta đã tạo bằng phần tử <template> ở trên.
Điều này dùng chính xác cùng một pattern như chúng ta đã thấy ở ví dụ đơn giản trước đó.
customElements.define(
"element-details",
class extends HTMLElement {
constructor() {
super();
const template = document.getElementById(
"element-details-template",
).content;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(document.importNode(template, true));
}
},
);
Dùng custom element <element-details> với named slots
Bây giờ hãy lấy phần tử <element-details> đó và thực sự dùng nó trong tài liệu của chúng ta:
<element-details>
<span slot="element-name">slot</span>
<span slot="description"
>A placeholder inside a web component that users can fill with their own
markup, with the effect of composing different DOM trees together.</span
>
<dl slot="attributes">
<dt>name</dt>
<dd>The name of the slot.</dd>
</dl>
</element-details>
<element-details>
<span slot="element-name">template</span>
<span slot="description"
>A mechanism for holding client- side content that is not to be rendered
when a page is loaded but may subsequently be instantiated during runtime
using JavaScript.</span
>
</element-details>
Về đoạn mã đó, hãy chú ý các điểm sau:
- Đoạn mã có hai thể hiện của phần tử
<element-details>, cả hai đều dùng thuộc tínhslotđể tham chiếu các named slots"element-name"và"description"mà chúng ta đặt trong shadow root của<element-details>. - Chỉ thể hiện đầu tiên trong hai phần tử
<element-details>đó tham chiếu named slot"attributes". Phần tử<element-details>thứ hai không có tham chiếu nào đến named slot"attributes". - Phần tử
<element-details>đầu tiên tham chiếu named slot"attributes"bằng một phần tử<dl>với các phần tử con<dt>và<dd>.
Thêm chút style cuối cùng
Để hoàn thiện, chúng ta sẽ thêm một chút CSS nữa cho các phần tử <dl>, <dt> và <dd> trong tài liệu:
dl {
margin-left: 6px;
}
dt {
color: #217ac0;
font-family: "Consolas", "Liberation Mono", "Courier New";
font-size: 110%;
font-weight: bold;
}
dd {
margin-left: 16px;
}
Kết quả
Cuối cùng, hãy ghép tất cả các đoạn mã lại và xem kết quả được render trông như thế nào.
Hãy chú ý các điểm sau về kết quả được render này:
- Mặc dù các thể hiện của phần tử
<element-details>trong tài liệu không trực tiếp dùng phần tử<details>, chúng vẫn được render bằng<details>vì shadow root làm cho chúng được điền nội dung tương ứng. - Trong đầu ra
<details>được render, nội dung trong các phần tử<element-details>lấp đầy các named slots từ shadow root. Nói cách khác, cây DOM từ các phần tử<element-details>được composed cùng với nội dung của shadow root. - Với cả hai phần tử
<element-details>, một tiêu đề Attributes được tự động thêm từ shadow root trước vị trí của named slot"attributes". - Vì phần tử
<element-details>đầu tiên có một phần tử<dl>tham chiếu rõ ràng tới named slot"attributes"từ shadow root của nó, nội dung của phần tử<dl>đó thay thế named slot"attributes"từ shadow root. - Vì phần tử
<element-details>thứ hai không tham chiếu rõ ràng tới named slot"attributes"từ shadow root của nó, nội dung của named slot đó sẽ được lấp bằng nội dung mặc định của nó từ shadow root. );
### Dùng custom element `<element-details>` với named slots
Bây giờ hãy lấy phần tử **`<element-details>`** đó và thực sự dùng nó trong tài liệu của chúng ta:
```html
<element-details>
<span slot="element-name">slot</span>
<span slot="description"
>A placeholder inside a web component that users can fill with their own
markup, with the effect of composing different DOM trees together.</span
>
<dl slot="attributes">
<dt>name</dt>
<dd>The name of the slot.</dd>
</dl>
</element-details>
<element-details>
<span slot="element-name">template</span>
<span slot="description"
>A mechanism for holding client- side content that is not to be rendered
when a page is loaded but may subsequently be instantiated during runtime
using JavaScript.</span
>
</element-details>
Về đoạn mã đó, hãy chú ý các điểm sau:
- Đoạn mã có hai thể hiện của phần tử
<element-details>, cả hai đều dùng thuộc tínhslotđể tham chiếu các named slots"element-name"và"description"mà chúng ta đặt trong shadow root của<element-details>. - Chỉ thể hiện đầu tiên trong hai phần tử
<element-details>đó tham chiếu named slot"attributes". Phần tử<element-details>thứ hai không có tham chiếu nào đến named slot"attributes". - Phần tử
<element-details>đầu tiên tham chiếu named slot"attributes"bằng một phần tử<dl>với các phần tử con<dt>và<dd>.
Thêm chút style cuối cùng
Để hoàn thiện, chúng ta sẽ thêm một chút CSS nữa cho các phần tử <dl>, <dt> và <dd> trong tài liệu:
dl {
margin-left: 6px;
}
dt {
color: #217ac0;
font-family: "Consolas", "Liberation Mono", "Courier New";
font-size: 110%;
font-weight: bold;
}
dd {
margin-left: 16px;
}
Kết quả
Cuối cùng, hãy ghép tất cả các đoạn mã lại và xem kết quả được render trông như thế nào.
Hãy chú ý các điểm sau về kết quả được render này:
- Mặc dù các thể hiện của phần tử
<element-details>trong tài liệu không trực tiếp dùng phần tử<details>, chúng vẫn được render bằng<details>vì shadow root làm cho chúng được điền nội dung tương ứng. - Trong đầu ra
<details>được render, nội dung trong các phần tử<element-details>lấp đầy các named slots từ shadow root. Nói cách khác, cây DOM từ các phần tử<element-details>được composed cùng với nội dung của shadow root. - Với cả hai phần tử
<element-details>, một tiêu đề Attributes được tự động thêm từ shadow root trước vị trí của named slot"attributes". - Vì phần tử
<element-details>đầu tiên có một phần tử<dl>tham chiếu rõ ràng tới named slot"attributes"từ shadow root của nó, nội dung của phần tử<dl>đó thay thế named slot"attributes"từ shadow root. - Vì phần tử
<element-details>thứ hai không tham chiếu rõ ràng tới named slot"attributes"từ shadow root của nó, nội dung của named slot đó sẽ được lấp bằng nội dung mặc định của nó từ shadow root.