关于模板的真相
当您需要在网页上反复重用相同的标记结构时,使用某种模板而不是一遍又一遍地重复相同的结构是有意义的。以前这是可能的,但 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 的默认内容。