範本、版位和陰影

網頁元件的優點在於可重複使用:您只需建立一次 UI 小工具,即可多次重複使用。儘管您需要 JavaScript 才能建立網頁元件,但不需要 JavaScript 程式庫。HTML 和相關 API 能提供您需要的所有資訊。

網頁元件標準由三個部分組成:HTML 範本自訂元素Shadow DOM。只要結合使用,您就能建構可順利整合到現有應用程式的自訂獨立元素 (封裝) 和可重複使用的元素,就像我們先前介紹的所有其他 HTML 元素一樣。

在本節中,我們會建立 <star-rating> 元素,讓使用者能夠以一到五顆星的分數為體驗評分。為自訂元素命名時,建議全部使用小寫英文字母。此外,請加入破折號,以便區分一般和自訂元素。

我們會說明如何使用 <template><slot> 元素、slot 屬性和 JavaScript 建立包含封裝的 Shadow DOM 範本。接下來,我們會重複使用定義的元素,並自訂一段文字,就如同您為任何元素或網頁元件一樣。我們也會簡短說明如何在自訂元素內部和外部使用 CSS。

<template> 元素

<template> 元素用於宣告要複製的 HTML 片段,並使用 JavaScript 插入 DOM。根據預設,系統不會顯示元素的內容。而是使用 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>

由於 <template> 元素的內容並未寫入畫面,因此不會顯示 <form> 及其內容。是的,這個 Codepen 沒有內容,但當你檢查 HTML 分頁時,就會看到 <template> 標記。

在這個範例中,<form> 不是 DOM 中 <template> 的子項。相反地,<template> 元素的內容是由 HTMLTemplateElement.content 屬性傳回的 DocumentFragment 子項。為了使系統顯示,必須使用 JavaScript 擷取內容,並將這些內容附加至 DOM。

這個簡短的 JavaScript 並未建立自訂元素。這個範例會將 <template> 的內容附加到 <body> 中。內容已成為可見且可設定樣式的 DOM 的一部分。

DOM 中顯示的舊版轉碼器的螢幕截圖。

需要 JavaScript 只實作一個星級評等的範本並不是最實用的用途,但是針對重複使用的可自訂星級評等小工具建立網頁元件,便相當實用。

<slot> 元素

我們納入一個版位,以包含自訂每個例項的圖例。HTML 會在 <template> 內提供 <slot> 元素做為預留位置,並在提供名稱時建立「已命名的版位」。已命名版位可用來自訂網頁元件中的內容。<slot> 元素可讓我們控制自訂元素的子項應插入其陰影樹狀結構的位置。

在我們的範本中,我們會將 <legend> 變更為 <slot>

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

如果元素的 slot 屬性值與已命名版位的名稱相符,則 name 屬性用於將版位指派給其他元素。如果自訂元素與版位沒有相符項目,則會顯示 <slot> 的內容。 因此,我們納入了包含一般內容的 <legend>,即使任何人只要在 HTML 中加入 <star-rating></star-rating> (但沒有內容),都能正常顯示。

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

slot 屬性是全域屬性,用於取代 <template> 中的 <slot> 內容。在自訂元素中,具有版位屬性的元素為 <legend>。但您不一定要這麼做。在範本中,<slot name="star-rating-legend"> 會替換為 <anyElement slot="star-rating-legend">,其中 <anyElement> 可以是任何元素,甚至是其他自訂元素。

未定義的元素

<template> 中,我們使用了 <rating> 元素。這不是自訂元素。而是未知元素。瀏覽器無法識別某個元素時也不會失敗。瀏覽器會將無法辨識的 HTML 元素視為匿名內嵌元素,該元素可透過 CSS 設定樣式。與 <span> 類似,<rating><star-rating> 元素沒有使用者代理程式套用的樣式或語意。

請注意,系統不會轉譯 <template> 和內容。<template> 是已知的元素,其中包含不顯示的內容。尚未定義 <star-rating> 元素。在我們定義元素前,瀏覽器會像所有無法辨識的元素一樣顯示。目前,無法辨識的 <star-rating> 會視為匿名內嵌元素,因此會顯示第三個 <star-rating> 中圖例和 <p> 的內容,與這些內容位於 <span> 中一樣。

我們可以定義 元素,將此無法辨識的元素轉換為自訂元素。

自訂元素

需要 JavaScript 才能定義自訂元素。定義後,<star-rating> 元素的內容會由陰影根取代,其中包含我們將相關聯範本的所有內容。範本中的 <slot> 元素會替換為 <star-rating> 中元素的內容,其中 slot 屬性值與 <slot> 的名稱值相符 (如果有的話)。否則,系統會顯示範本版位的內容。

與版位相關聯的自訂元素內容 (我們的第三個 <star-rating> 中的 <p>Is this text visible?</p>) 不會納入陰影根層級,因此不會顯示。

我們透過擴充 HTMLElement定義名為 star-rating 的自訂元素

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));
    }
  });

現在,該元素已定義完畢,每次瀏覽器遇到 <star-rating> 元素時,就會以元素定義的 #star-rating-template (這是我們的範本) 定義呈現。瀏覽器會將陰影 DOM 樹狀結構附加至節點,將範本內容的「複製」附加至該 shadow DOM。請注意,您attachShadow() 可以使用的元素受到限制

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

如果您查看開發人員工具,就會留意 <template><form> 是每個自訂元素的陰影根部分。在開發人員工具中的每個自訂元素中,<template> 內容的複本都會顯示,並且會顯示在瀏覽器中,但自訂元素本身的內容不會顯示在畫面上。

開發人員工具螢幕截圖:顯示每個自訂元素中複製的範本內容。

<template> 範例中,我們將範本內容附加至文件主體,並將內容新增至一般 DOM。在 customElements 定義中,我們使用相同的 appendChild(),但複製的範本內容已附加至封裝的 shadow DOM。

您是否注意到,星星回到未設定樣式的圓形按鈕?Codepen CSS 分頁中的樣式屬於 shadow DOM 的一部分,而非標準 DOM。該分頁的 CSS 樣式的範圍僅限於文件,而非 shadow DOM,因此系統不會套用樣式。我們必須建立封裝的樣式,為封裝的 Shadow DOM 內容設定樣式。

陰影 DOM

陰影 DOM 將 CSS 樣式設定至每個陰影樹狀結構,隔離文件的其餘部分。也就是說,外部 CSS 不適用於您的元件,而元件樣式對於文件的其餘部分不會產生任何影響,除非我們刻意將這類樣式導向至您的元件。

由於我們已將內容附加到 shadow DOM,因此可以加入 <style> 元素,將封裝的 CSS 提供給自訂元素。

只要將範圍限定在自訂元素,我們就不必擔心文件其他部分顯示的樣式。我們可以大幅減少選取器的明確性。舉例來說,由於自訂元素中唯一使用的輸入是圓形按鈕,因此我們可以使用 input 而非 input[type="radio"] 做為選取器。

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

雖然網頁元件已封裝在 <template> 中的標記,而 CSS 樣式的範圍限定為 shadow DOM,而且在元件外隱藏,所以轉譯的運算單元內容 (<star-rating><anyElement slot="star-rating-legend"> 部分) 不會封裝。

在目前範圍外設定樣式

您也可以直接在 shadow DOM 中設定文件樣式,以及根據全域樣式設定 shadow DOM 的內容樣式。陰影邊界 (陰影 DOM 結束且一般 DOM 開始) 可以周遊,但只有非常刻意。

陰影樹狀結構是 shadow DOM 中的 DOM 樹狀結構。陰影根是陰影樹狀結構的根節點。

:host 虛擬類別會選取 <star-rating> (陰影主機元素)。「陰影主機」是陰影 DOM 附加的 DOM 節點。如果只要指定主機的特定版本,請使用 :host()。這只會選取與傳遞的參數相符的陰影主機元素,例如類別或屬性選取器。如要選取所有自訂元素,您可以在全域 CSS 中使用 star-rating { /* styles */ },或在範本樣式中使用 :host(:not(#nonExistantId))具體性來說,全球 CSS 供應商勝出。

::slotted() 虛擬元素會從 shadow DOM 跨越陰影 DOM 邊界。如果該元素與選取器相符,就會選取版位化元素。在本範例中,::slotted(legend) 與三個圖例相符。

如要從全域範圍指定 CSS 的 shadow DOM,您必須修改範本。您可以將 part 屬性新增至要設定樣式的任何元素。接著,使用 ::part() 虛擬元素,比對陰影樹狀結構中與傳遞的參數相符的元素。虛擬元素的錨定或來源元素是主機或自訂元素名稱,在本例中為 star-rating。參數是 part 屬性的值。

如果範本標記的開頭為:

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

我們可以將 <form><fieldset> 指定如下:

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

部分名稱的運作方式與類別類似:元素可以有多個以空格分隔的部分名稱,而多個元素可以具有相同的部分名稱。

Google 提供了很棒的建立自訂元素檢查清單。我們也建議您瞭解「宣告式陰影 DOM」。

隨堂測驗

測驗您對範本、版位和陰影的相關知識。

根據預設,shadow DOM 外部的樣式會為其中的元素設定樣式。

正確。
請再試一次。
不正確。
答對了!

以下何者是 <template> 元素的正確說明?

用來顯示您網頁中任何內容的通用元素。
請再試一次。
預留位置元素。
請再試一次。
用來宣告 HTML 片段的元素 (根據預設並不會顯示)。
答對了!