關於模板的真相
當您需要在網頁上反覆重用相同的標記結構時,使用某種模板而不是一遍又一遍地重複相同的結構是有意義的。以前這是可能的,但 HTML <template> 元素使這一切變得容易得多。此元素及其內容不會在 DOM 中渲染,但仍然可以使用 JavaScript 引用它。
讓我們看一個非常簡單的快速示例
<template id="custom-paragraph">
<p>My paragraph</p>
</template>
在您透過 JavaScript 獲取對它的引用並使用類似以下的方法將其附加到 DOM 之前,它不會出現在您的頁面上
let template = document.getElementById("custom-paragraph");
let templateContent = template.content;
document.body.appendChild(templateContent);
儘管很簡單,但您已經可以看到這有多麼有用。
將模板與 Web 元件一起使用
模板本身很有用,但它們與 Web 元件結合使用效果更好。讓我們定義一個使用我們的模板作為其 Shadow DOM 內容的 Web 元件。我們也將它命名為 <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(templateContent.cloneNode(true));
}
},
);
這裡需要注意的關鍵點是,我們使用 Node.cloneNode() 方法建立的模板內容的克隆體附加到 Shadow Root。
由於我們將模板內容附加到 Shadow DOM,因此我們可以在模板內包含樣式資訊,在 <style> 元素中,然後將其封裝在自定義元素內。如果我們只是將其附加到標準 DOM,這將不起作用。
所以例如:
<template id="custom-paragraph">
<style>
p {
color: white;
background-color: #666666;
padding: 5px;
}
</style>
<p>My paragraph</p>
</template>
現在,我們只需將其新增到 HTML 文件中即可使用它。
<my-paragraph></my-paragraph>
透過插槽新增靈活性
到目前為止一切順利,但該元素並不十分靈活。我們只能在其內部顯示一小段文字,這意味著目前它的用處甚至不如一個普通的段落!我們可以使用 <slot> 元素以一種不錯的宣告式方式,使每個元素例項都可以顯示不同的文字。
插槽透過其 name 屬性進行標識,並允許您在模板中定義佔位符,當元素在標記中使用時,可以使用您想要的任何標記片段來填充這些佔位符。
因此,如果我們想在我們的簡單示例中新增一個插槽,我們可以像這樣更新模板的段落元素。
<p><slot name="my-text">My default text</slot></p>
如果元素在標記中包含時未定義插槽的內容,或者瀏覽器不支援插槽,則 <my-paragraph> 只包含預設內容“My default text”。
要定義插槽的內容,我們在 <my-paragraph> 元素內包含一個 HTML 結構,其中包含一個 slot 屬性,其值等於我們要填充的插槽的名稱。和以前一樣,這可以是您喜歡的任何內容,例如
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
</my-paragraph>
or
<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 屬性的元素填充。
name 和 slot 屬性都預設為空字串,因此沒有 slot 屬性的元素會被分配給沒有 name 屬性的 <slot>(未命名插槽,或預設插槽)。這是一個例子。
<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>
然後,您可以這樣使用它:
<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,其中包含一些 命名插槽。
<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>
這個 <template> 元素具有幾個特性:
-
該
<template>包含一個<style>元素,其中包含一組 CSS 樣式,這些樣式僅限於<template>建立的文件片段。 -
該
<template>使用<slot>及其name屬性建立了三個 命名插槽。<slot name="element-name"><slot name="description"><slot name="attributes">
-
該
<template>將 命名插槽 包裝在一個<details>元素中。
從 <template> 建立新的 <element-details> 元素
接下來,讓我們建立一個名為 <element-details> 的新自定義元素,並使用 Element.attachShadow 將上面用 <template> 元素建立的文件片段作為其 Shadow Root 附加到它。這使用了與我們早期簡單示例完全相同的模式。
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> 元素並在我們的文件中使用它。
<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。
dl {
margin-left: 6px;
}
dt {
color: #217ac0;
font-family: "Consolas", "Liberation Mono", "Courier New";
font-size: 110%;
font-weight: bold;
}
dd {
margin-left: 16px;
}
結果
最後,讓我們將所有程式碼片段放在一起,看看渲染結果是什麼樣的。
關於這個渲染結果,請注意以下幾點:
- 儘管文件中的
<element-details>元素例項沒有直接使用<details>元素,但它們會使用<details>進行渲染,因為 Shadow Root 使它們被填充了該內容。 - 在渲染的
<details>輸出中,<element-details>元素中的內容填充了 Shadow Root 中的 命名插槽。換句話說,來自<element-details>元素的 DOM 樹與 Shadow Root 的內容被 *組合* 在了一起。 - 對於兩個
<element-details>元素,一個 Attributes 標題會自動從 Shadow Root 新增到"attributes"命名插槽 的位置之前。 - 因為第一個
<element-details>包含一個<dl>元素,該元素顯式地從其 Shadow Root 引用"attributes"命名插槽,所以該<dl>的內容會替換 Shadow Root 中的"attributes"命名插槽。 - 因為第二個
<element-details>沒有顯式地從其 Shadow Root 引用"attributes"命名插槽,所以它針對該 命名插槽 的內容會填充來自 Shadow Root 的預設內容。