使用模板和插槽

本文解释了如何使用 <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;
}

结果

最后,让我们将所有代码片段放在一起,看看渲染结果是什么样的。

关于这个渲染结果,请注意以下几点: