使用模板和插槽

本文解釋瞭如何使用 <template><slot> 元素來建立一個靈活的模板,然後可以使用該模板來填充 Web 元件的 Shadow DOM。

關於模板的真相

當您需要在網頁上反覆重用相同的標記結構時,使用某種模板而不是一遍又一遍地重複相同的結構是有意義的。以前這是可能的,但 HTML <template> 元素使這一切變得容易得多。此元素及其內容不會在 DOM 中渲染,但仍然可以使用 JavaScript 引用它。

讓我們看一個非常簡單的快速示例

html
<template id="custom-paragraph">
  <p>My paragraph</p>
</template>

在您透過 JavaScript 獲取對它的引用並使用類似以下的方法將其附加到 DOM 之前,它不會出現在您的頁面上

js
let template = document.getElementById("custom-paragraph");
let templateContent = template.content;
document.body.appendChild(templateContent);

儘管很簡單,但您已經可以看到這有多麼有用。

將模板與 Web 元件一起使用

模板本身很有用,但它們與 Web 元件結合使用效果更好。讓我們定義一個使用我們的模板作為其 Shadow DOM 內容的 Web 元件。我們也將它命名為 <my-paragraph>

js
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(templateContent.cloneNode(true));
    }
  },
);

這裡需要注意的關鍵點是,我們使用 Node.cloneNode() 方法建立的模板內容的克隆體附加到 Shadow Root。

由於我們將模板內容附加到 Shadow DOM,因此我們可以在模板內包含樣式資訊,在 <style> 元素中,然後將其封裝在自定義元素內。如果我們只是將其附加到標準 DOM,這將不起作用。

所以例如:

html
<template id="custom-paragraph">
  <style>
    p {
      color: white;
      background-color: #666666;
      padding: 5px;
    }
  </style>
  <p>My paragraph</p>
</template>

現在,我們只需將其新增到 HTML 文件中即可使用它。

html
<my-paragraph></my-paragraph>

透過插槽新增靈活性

到目前為止一切順利,但該元素並不十分靈活。我們只能在其內部顯示一小段文字,這意味著目前它的用處甚至不如一個普通的段落!我們可以使用 <slot> 元素以一種不錯的宣告式方式,使每個元素例項都可以顯示不同的文字。

插槽透過其 name 屬性進行標識,並允許您在模板中定義佔位符,當元素在標記中使用時,可以使用您想要的任何標記片段來填充這些佔位符。

因此,如果我們想在我們的簡單示例中新增一個插槽,我們可以像這樣更新模板的段落元素。

html
<p><slot name="my-text">My default text</slot></p>

如果元素在標記中包含時未定義插槽的內容,或者瀏覽器不支援插槽,則 <my-paragraph> 只包含預設內容“My default text”。

要定義插槽的內容,我們在 <my-paragraph> 元素內包含一個 HTML 結構,其中包含一個 slot 屬性,其值等於我們要填充的插槽的名稱。和以前一樣,這可以是您喜歡的任何內容,例如

html
<my-paragraph>
  <span slot="my-text">Let's have some different text!</span>
</my-paragraph>

or

html
<my-paragraph>
  <ul slot="my-text">
    <li>Let's have some different text!</li>
    <li>In a list!</li>
  </ul>
</my-paragraph>

注意: 可以插入到插槽中的節點稱為“可插入節點”(Slottable nodes);當一個節點被插入到插槽中時,它被稱為“已插入”(slotted)。

這就是我們的簡單示例。如果您想進一步嘗試,可以在 GitHub 上找到它(也可以 線上檢視)。

name 屬性在每個 Shadow Root 中都應該是唯一的:如果您有兩個同名的插槽,所有具有匹配 slot 屬性的元素都將分配給第一個具有該名稱的插槽。但是 slot 屬性不需要唯一:一個 <slot> 可以被多個具有匹配 slot 屬性的元素填充。

nameslot 屬性都預設為空字串,因此沒有 slot 屬性的元素會被分配給沒有 name 屬性的 <slot>(未命名插槽,或預設插槽)。這是一個例子。

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

然後,您可以這樣使用它:

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

在此示例中

  • slot="my-text" 的內容進入命名插槽。
  • 所有其他內容將自動進入未命名插槽。

一個更復雜的示例

為了完成本文,讓我們看一個稍微複雜一點的東西。

以下程式碼片段展示瞭如何將 <slot><template> 和一些 JavaScript 結合使用,以:

  • 建立一個帶有 命名插槽<element-details> 元素,這些插槽位於其 Shadow Root 中。
  • 以這樣一種方式設計 <element-details> 元素,當它在文件中使用時,它將透過將元素的 Shadow Root 的內容與元素的內容組合起來進行渲染——也就是說,元素內容的某些部分用於填充其 Shadow Root 中的 命名插槽

請注意,在沒有 <template> 元素的情況下使用 <slot> 元素在技術上是可能的,例如在普通的 <div> 元素中,並且仍然可以利用 <slot> 對 Shadow DOM 內容的佔位符功能,這樣做可以避免需要先訪問模板元素的 content 屬性(並克隆它)的小麻煩。然而,通常將插槽新增到 <template> 元素中更為實用,因為您不太可能需要基於已渲染的元素定義模式。

此外,即使它尚未渲染,當使用 <template> 時,容器作為模板的用途在語義上會更加清晰。此外,<template> 可以直接新增項,例如 <td>,它們在新增到 <div> 時會消失。

注意: 您可以在 element-details(也可以 線上檢視)找到這個完整的示例。

建立一個帶有某些插槽的模板

首先,我們在 <template> 元素中使用 <slot> 元素來建立一個新的“element-details-template” DocumentFragment,其中包含一些 命名插槽

html
<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"
          >&lt;<slot name="element-name">NEED NAME</slot>&gt;</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>

這個 <template> 元素具有幾個特性:

從 <template> 建立新的 <element-details> 元素

接下來,讓我們建立一個名為 <element-details> 的新自定義元素,並使用 Element.attachShadow 將上面用 <template> 元素建立的文件片段作為其 Shadow Root 附加到它。這使用了與我們早期簡單示例完全相同的模式。

js
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(template.cloneNode(true));
    }
  },
);

使用帶有命名插槽的 <element-details> 自定義元素

現在,讓我們使用這個 <element-details> 元素並在我們的文件中使用它。

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>

關於這段程式碼片段,請注意以下幾點:

  • 該程式碼片段包含兩個 <element-details> 元素的例項,它們都使用 slot 屬性引用我們在 <element-details> Shadow Root 中放置的 "element-name""description" 命名插槽
  • 在這兩個 <element-details> 元素中,只有第一個引用了 "attributes" 命名插槽。第二個 <element-details> 元素沒有任何對 "attributes" 命名插槽 的引用。
  • 第一個 <element-details> 元素使用帶有 <dt><dd> 子元素的 <dl> 元素來引用 "attributes" 命名插槽

新增最後的樣式

作為最後的潤色,我們將為文件中的 <dl><dt><dd> 元素新增一點 CSS。

css
dl {
  margin-left: 6px;
}
dt {
  color: #217ac0;
  font-family: "Consolas", "Liberation Mono", "Courier New";
  font-size: 110%;
  font-weight: bold;
}
dd {
  margin-left: 16px;
}

結果

最後,讓我們將所有程式碼片段放在一起,看看渲染結果是什麼樣的。

關於這個渲染結果,請注意以下幾點: