Mẫu, vùng và bóng

Lợi ích của các thành phần web là khả năng tái sử dụng: bạn có thể tạo một tiện ích giao diện người dùng một lần và sử dụng lại nhiều lần. Mặc dù cần JavaScript để tạo các thành phần web, nhưng bạn không cần thư viện JavaScript. HTML và các API liên quan cung cấp mọi thứ bạn cần.

Tiêu chuẩn của Thành phần web bao gồm ba phần: Mẫu HTML, Phần tử tuỳ chỉnhDOM bóng. Khi kết hợp, chúng cho phép xây dựng các phần tử tuỳ chỉnh, độc lập (đóng gói), có thể sử dụng lại và có thể tích hợp liền mạch vào các ứng dụng hiện có, như tất cả các phần tử HTML khác mà chúng tôi đã đề cập.

Trong phần này, chúng ta sẽ tạo phần tử <star-rating>, một thành phần web cho phép người dùng xếp hạng trải nghiệm theo thang điểm từ 1 đến 5 sao. Khi đặt tên cho phần tử tuỳ chỉnh, bạn nên sử dụng tất cả chữ thường. Ngoài ra, hãy thêm dấu gạch ngang để phân biệt giữa phần tử thông thường và phần tử tuỳ chỉnh.

Chúng ta sẽ thảo luận về cách sử dụng các phần tử <template><slot>, thuộc tính slot và JavaScript để tạo một mẫu với DOM tối được đóng gói. Sau đó, chúng ta sẽ sử dụng lại phần tử đã xác định, tuỳ chỉnh một phần văn bản, giống như cách bạn làm với bất kỳ phần tử hoặc thành phần web nào. Chúng ta cũng sẽ thảo luận ngắn gọn về việc sử dụng CSS bên trong và bên ngoài phần tử tuỳ chỉnh.

Phần tử <template>

Phần tử <template> được dùng để khai báo các đoạn HTML sẽ được sao chép và chèn vào DOM bằng JavaScript. Nội dung của phần tử không được hiển thị theo mặc định. Thay vào đó, các lớp này được tạo thực thể bằng JavaScript.

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Vì nội dung của phần tử <template> không được ghi lên màn hình nên <form> và nội dung của phần tử này không được kết xuất. Đúng, Codepen này trống, nhưng nếu kiểm tra thẻ HTML, bạn sẽ thấy mã đánh dấu <template>.

Trong ví dụ này, <form> không phải là phần tử con của <template> trong DOM. Thay vào đó, nội dung của các phần tử <template> là phần tử con của DocumentFragment do thuộc tính HTMLTemplateElement.content trả về. Để hiển thị, bạn phải sử dụng JavaScript để lấy nội dung và nối những nội dung đó vào DOM.

JavaScript ngắn này không tạo phần tử tuỳ chỉnh. Thay vào đó, ví dụ này đã thêm nội dung của <template> vào <body>. Nội dung đã trở thành một phần của DOM hiển thị và có thể định kiểu.

Ảnh chụp màn hình của chương trình soạn thảo mã trước đó như minh hoạ trong DOM.

Việc yêu cầu JavaScript để triển khai mẫu cho chỉ một điểm xếp hạng theo sao là không hữu ích, nhưng việc tạo một thành phần web cho một tiện ích xếp hạng theo sao có thể tuỳ chỉnh và được sử dụng nhiều lần sẽ hữu ích.

Phần tử <slot>

Chúng tôi sẽ thêm một vùng chú giải cho mỗi lần xuất hiện. HTML cung cấp phần tử <slot> dưới dạng phần giữ chỗ bên trong <template>. Phần tử này sẽ tạo một "vùng được đặt tên" nếu được cung cấp. Bạn có thể sử dụng một khe được đặt tên để tuỳ chỉnh nội dung trong một thành phần web. Phần tử <slot> cung cấp cho chúng ta một cách để kiểm soát vị trí chèn thành phần con của một thành phần tuỳ chỉnh trong cây bóng đổ.

Trong mẫu của chúng ta, chúng ta thay đổi <legend> thành <slot>:

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

Thuộc tính name được dùng để gán vị trí cho các phần tử khác nếu phần tử đó có thuộc tính slot có giá trị khớp với tên của một vị trí đã đặt tên. Nếu phần tử tuỳ chỉnh không phù hợp với một vị trí, nội dung của <slot> sẽ được hiển thị. Vì vậy, chúng tôi đã đưa <legend> vào với nội dung chung chung có thể được kết xuất nếu có ai đó chỉ bao gồm <star-rating></star-rating> (không có nội dung) trong HTML của họ.

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

Thuộc tính slot là một thuộc tính chung dùng để thay thế nội dung của <slot> trong <template>. Trong phần tử tuỳ chỉnh của chúng ta, phần tử có thuộc tính vị trí là <legend>. Không nhất thiết phải là như vậy. Trong mẫu của chúng tôi, <slot name="star-rating-legend"> sẽ được thay thế bằng <anyElement slot="star-rating-legend">, trong đó <anyElement> có thể là phần tử bất kỳ, thậm chí là một phần tử tuỳ chỉnh khác.

Phần tử không xác định

Trong <template>, chúng ta đã sử dụng phần tử <rating>. Đây không phải là phần tử tuỳ chỉnh. Đúng hơn, đó là một phần tử không xác định. Các trình duyệt sẽ không bị lỗi khi không nhận ra phần tử nào đó. Trình duyệt coi các phần tử HTML không nhận dạng được là phần tử cùng dòng ẩn danh và có thể được tạo kiểu bằng CSS. Tương tự như <span>, các phần tử <rating><star-rating> không có kiểu hoặc ngữ nghĩa áp dụng cho tác nhân người dùng.

Lưu ý rằng <template> và nội dung không được kết xuất. <template> là một phần tử đã biết chứa nội dung không được kết xuất. Phần tử <star-rating> vẫn chưa được xác định. Cho đến khi chúng ta xác định một phần tử, trình duyệt sẽ hiển thị phần tử đó giống như tất cả các phần tử không nhận dạng được. Hiện tại, <star-rating> không nhận dạng được sẽ được coi là phần tử cùng dòng ẩn danh. Vì vậy, nội dung bao gồm cả chú giải và <p> trong <star-rating> thứ ba sẽ hiển thị như khi chúng hiển thị trong <span>.

Hãy xác định phần tử của chúng ta để chuyển đổi phần tử không xác định này thành phần tử tuỳ chỉnh.

Phần tử tùy chỉnh

Cần có JavaScript để xác định các phần tử tuỳ chỉnh. Khi được xác định, nội dung của phần tử <star-rating> sẽ được thay thế bằng một gốc bóng chứa tất cả nội dung của mẫu mà chúng tôi liên kết với mẫu đó. Các phần tử <slot> từ mẫu sẽ được thay thế bằng nội dung của phần tử trong <star-rating> có giá trị thuộc tính slot khớp với giá trị tên của <slot> (nếu có). Nếu không, nội dung của các vùng mẫu sẽ hiển thị.

Nội dung trong một phần tử tuỳ chỉnh không liên kết với một vị trí (<p>Is this text visible?</p> trong <star-rating> thứ ba của chúng tôi) không được đưa vào gốc bóng đổ và do đó không hiển thị.

Chúng tôi xác định phần tử tuỳ chỉnh có tên star-rating bằng cách mở rộng HTMLElement:

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

Phần tử hiện đã được xác định, mỗi khi trình duyệt gặp một phần tử <star-rating>, phần tử đó sẽ hiển thị như đã được phần tử có #star-rating-template, mẫu của chúng tôi xác định. Trình duyệt sẽ đính kèm cây DOM bóng vào nút, thêm bản sao của nội dung mẫu vào DOM bóng đó. Lưu ý rằng các phần tử mà bạn có thể dùng attachShadow() bị giới hạn.

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

Nếu xem công cụ cho nhà phát triển, bạn sẽ thấy <form> từ <template> thuộc gốc đổ bóng của mỗi phần tử tuỳ chỉnh. Bản sao của nội dung <template> rõ ràng trong từng phần tử tuỳ chỉnh của các công cụ cho nhà phát triển và hiển thị trong trình duyệt, nhưng nội dung của chính phần tử tuỳ chỉnh đó không hiển thị lên màn hình.

Ảnh chụp màn hình Công cụ cho nhà phát triển cho thấy nội dung mẫu được sao chép trong mỗi phần tử tuỳ chỉnh.

Trong ví dụ <template>, chúng ta đã thêm nội dung mẫu vào nội dung tài liệu, thêm nội dung vào DOM thông thường. Trong định nghĩa customElements, chúng tôi sử dụng cùng một appendChild(), nhưng nội dung mẫu sao chép được thêm vào một DOM bóng được đóng gói.

Bạn có biết các ngôi sao đã trở thành các nút chọn chưa được định kiểu như thế nào không? Là một phần của DOM bóng thay vì DOM chuẩn, kiểu trong thẻ CSS của Codepen không áp dụng. Kiểu CSS của thẻ đó nằm trong phạm vi của tài liệu, không phải trong DOM tối, vì vậy kiểu không được áp dụng. Chúng ta phải tạo các kiểu đóng gói để tạo kiểu cho nội dung DOM bóng được đóng gói.

DOM bóng

DOM bóng đổ phạm vi các kiểu CSS cho từng cây bóng, tách riêng nó với phần còn lại của tài liệu. Điều này có nghĩa là CSS bên ngoài không áp dụng cho thành phần của bạn và các kiểu thành phần sẽ không ảnh hưởng đến phần còn lại của tài liệu, trừ khi chúng tôi cố ý chuyển hướng các kiểu đó đến thành phần đó.

Vì đã thêm nội dung vào DOM bóng, nên chúng ta có thể thêm phần tử <style> cung cấp CSS đóng gói cho phần tử tuỳ chỉnh.

Do trong phạm vi của phần tử tuỳ chỉnh, chúng ta không phải lo lắng về việc các kiểu xuất hiện trong phần còn lại của tài liệu. Chúng ta có thể làm giảm đáng kể tính đặc thù của các bộ chọn. Ví dụ: vì dữ liệu đầu vào duy nhất được sử dụng trong phần tử tuỳ chỉnh là các nút chọn, nên chúng ta có thể sử dụng input thay vì input[type="radio"] làm bộ chọn.

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Mặc dù các thành phần web được đóng gói bằng mã đánh dấu trong <template>, còn kiểu CSS thì nằm trong phạm vi của DOM bóng và bị ẩn khỏi mọi thứ bên ngoài thành phần, nhưng nội dung vị trí được hiển thị (phần <anyElement slot="star-rating-legend"> của <star-rating>) lại không được đóng gói.

Tạo kiểu bên ngoài phạm vi hiện tại

Có thể, nhưng không đơn giản, có thể tạo kiểu cho tài liệu từ bên trong DOM bóng và tạo kiểu cho nội dung của DOM bóng từ kiểu chung. ranh giới bóng, nơi DOM bóng kết thúc và DOM thông thường bắt đầu, có thể được truyền tải, nhưng chỉ rất có chủ đích.

Cây bóng là cây DOM bên trong DOM bóng. Gốc bóng đổ là nút gốc của cây bóng đổ.

Lớp giả :host chọn <star-rating>, phần tử máy chủ lưu trữ bóng. Máy chủ bóng là nút DOM mà DOM bóng được đính kèm vào. Để chỉ nhắm đến các phiên bản cụ thể của máy chủ lưu trữ, hãy sử dụng :host(). Thao tác này sẽ chỉ chọn các phần tử máy chủ lưu trữ bóng khớp với tham số được truyền, chẳng hạn như bộ chọn lớp hoặc thuộc tính. Để chọn tất cả các phần tử tuỳ chỉnh, bạn có thể sử dụng star-rating { /* styles */ } trong CSS chung hoặc :host(:not(#nonExistantId)) trong kiểu mẫu. Xét về tính cụ thể, CSS toàn cầu sẽ chiến thắng.

Phần tử giả ::slotted() vượt qua ranh giới của DOM bóng từ bên trong DOM bóng. Phần tử này sẽ chọn một phần tử có rãnh nếu phần tử đó khớp với bộ chọn. Trong ví dụ này, ::slotted(legend) khớp với 3 chú giải.

Để nhắm đến một DOM bóng từ CSS trong phạm vi toàn cầu, bạn cần phải chỉnh sửa mẫu. Bạn có thể thêm thuộc tính part vào bất kỳ phần tử nào mà bạn muốn tạo kiểu. Sau đó, hãy sử dụng phần tử giả ::part() để so khớp các phần tử trong cây bóng đổ khớp với tham số được truyền. Phần tử neo hoặc phần tử gốc cho phần tử giả là tên máy chủ lưu trữ hoặc tên phần tử tuỳ chỉnh, trong trường hợp này là star-rating. Tham số là giá trị của thuộc tính part.

Nếu mã đánh dấu mẫu của chúng tôi bắt đầu như vậy:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

Chúng ta có thể nhắm mục tiêu <form><fieldset> bằng:

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

Tên phần hoạt động tương tự như lớp: một phần tử có thể có nhiều tên phần được phân tách bằng dấu cách và nhiều phần tử có thể có cùng tên phần.

Google có một danh sách kiểm tra tuyệt vời để tạo phần tử tuỳ chỉnh. Bạn cũng nên tìm hiểu về DOM tối khai báo.

Kiểm tra mức độ hiểu biết của bạn

Kiểm tra kiến thức của bạn về mẫu, ô và bóng.

Theo mặc định, kiểu từ bên ngoài DOM bóng sẽ tạo kiểu cho các thành phần bên trong.

Đúng.
Hãy thử lại.
Sai.
Chính xác!

Câu trả lời nào mô tả đúng về phần tử <template>?

Một phần tử chung được sử dụng để hiển thị bất kỳ nội dung nào trong trang của bạn.
Hãy thử lại.
Phần tử giữ chỗ.
Hãy thử lại.
Một phần tử được dùng để khai báo các đoạn của HTML và phần tử này sẽ không hiển thị theo mặc định.
Chính xác!