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

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

Tiêu chuẩn Thành phần web bao gồm ba phần: mẫu HTML, Phần tử tuỳ chỉnhDOM tối. 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ể tái sử dụng 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 đánh giá một trải nghiệm trên thang điểm từ 1 đến 5 sao. Khi đặt tên cho một phần tử tuỳ chỉnh, bạn nên sử dụng tất cả chữ cái viết thường. Ngoài ra, hãy thêm một dấu gạch ngang, vì điều này giúp 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 có một DOM tối được đóng gói. Sau đó chúng tôi 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ư 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 trong và ngoài phần tử tuỳ chỉnh.

Phần tử <template>

Phần tử <template> dùng để khai báo các phân đoạn HTML sẽ được sao chép và chèn vào DOM bằng JavaScript. Theo mặc định, nội dung của phần tử không hiển thị. Thay vào đó, chúng đượ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ử đó không được hiển thị. Đú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 trong DocumentFragment được HTMLTemplateElement.content trả về thuộc tính này. Để được hiển thị, JavaScript phải được sử dụng để lấy nội dung và nối các nội dung đó vào DOM.

JavaScript ngắn này không tạo ra 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ể tạo kiểu.

Ảnh chụp màn hình của bút mã trước đó như hiển thị trong DOM.

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

Phần tử <slot>

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

Trong mẫu của mình, 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 dùng để gán ô 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 vùng được đặt tên. Nếu phần tử tuỳ chỉnh không có kết quả phù hợp cho một vị trí, thì nội dung của <slot> sẽ hiển thị. Vì vậy, chúng ta đã đưa một <legend> vào nội dung chung chung có thể hiển thị nếu có ai đó chỉ cần đưa <star-rating></star-rating> (không có nội dung) vào 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 được sử dụng để thay thế nội dung của <slot> trong <template>. Trong phần tử tùy chỉnh của chúng tôi, phần tử có thuộc tính vị trí là một <legend>. Không nhất thiết phải như vậy. Trong mẫu của chúng ta, <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à một phần tử tuỳ chỉnh. Đúng hơn, đó là một phần tử không xác định. Trình duyệt không bị lỗi khi không nhận ra một phần tử. Các phần tử HTML không được nhận dạng sẽ được trình duyệt coi là nội tuyến ẩn danh 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 áp dụng tác nhân người dùng kiểu hoặc ngữ nghĩa.

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

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

Phần tử tùy chỉnh

JavaScript là bắt buộc để 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 có bóng chứa tất cả nội dung của mẫu chúng tôi liên kết với mẫu đó. Các phần tử <slot> trong mẫu đã được thay thế với 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 thì có một sản phẩm như vậy. Nếu không, nội dung trong vùng mẫu sẽ hiển thị.

Nội dung trong một phần tử tuỳ chỉnh không được 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 được hiển thị.

Chúng ta xác định phần tử tuỳ chỉnh có tên là 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));
    }
  });

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

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

Nếu xem các công cụ cho nhà phát triển, bạn sẽ nhận thấy <form> trong <template> là một phần của gốc bóng đổ của mỗi phần tử tuỳ chỉnh. Bản sao nội dung <template> xuất hiện rõ ràng trong mỗi phần tử tuỳ chỉnh trong các công cụ cho nhà phát triển và xuất hiện trong trình duyệt, nhưng nội dung là 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ụ về <template>, chúng ta đã thêm nội dung mẫu vào phần 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 ta đã sử dụng cùng một appendChild(), nhưng nội dung mẫu sao chép đã được thêm vào DOM bóng được đóng gói.

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

DOM bóng

DOM bóng đặt phạm vi các kiểu CSS cho từng cây bóng, tách biệt kiểu CSS đó 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, đồng thời các kiểu thành phần không có ảnh hưởng đến phần còn lại của tài liệu, trừ phi chúng tôi cố ý đưa người xem đến.

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

Do thuộc phạm vi của phần tử tuỳ chỉnh nên chúng ta không phải lo lắng về việc các kiểu lan sang 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 bộ chọn. Ví dụ: vì đầu vào duy nhất được sử dụng trong phần tử tùy chỉnh là radio thì 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 gói gọn với mã đánh dấu trong <template> và kiểu CSS nằm trong phạm vi DOM tối và bị ẩn từ mọi thứ bên ngoài thành phần, nội dung vị trí được hiển thị, <anyElement slot="star-rating-legend"> của <star-rating>, 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, để tạo kiểu cho tài liệu từ bên trong DOM tối và tạo kiểu cho nội dung của DOM tối từ các 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ể di chuyển, nhưng chỉ một cách có 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> là phần tử máy chủ bóng đổ. Máy chủ bóng là nút DOM mà DOM tối được đính kèm. Để 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 những phần tử 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ả 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. Về mặt về tính cụ thể, thì CSS toàn cầu sẽ giành chiến thắng.

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

Để nhắm đến DOM tối từ CSS ở phạm vi toàn cầu, bạn cần chỉnh sửa mẫu. part có thể thêm vào bất kỳ phần tử nào bạn muốn tạo kiểu. Sau đó, dùng phần tử giả ::part() để khớp với 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 của phần tử giả là tên máy chủ hoặc tên phần tử tuỳ chỉnh, trong trường hợp này là star-rating. Tham số này là giá trị của thuộc tính part.

Nếu hệ thống đá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ể chứa 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 bộ phận.

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

Kiểm tra kiến thức

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

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

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

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

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