使用模板和插槽

本文介绍了如何使用 <template><slot> 元素创建灵活的模板,然后将其用于填充 Web Components 的 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 Components 中使用模板

模板本身很有用,但它们与 Web Components 一起使用效果更好。让我们定义一个 Web Components,它使用我们的模板作为其 Shadow DOM 的内容。我们也将其称为 <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));
    }
  },
);

这里需要注意的关键点是,我们将模板内容的克隆附加到 Shadow Root,使用 Node.cloneNode() 方法创建。

并且因为我们将它的内容附加到 Shadow DOM,所以我们可以在模板中的 <style> 元素内包含一些样式信息,然后将其封装在自定义元素中。如果我们只是将其附加到标准 DOM,则此方法将不起作用。

例如

html
<template id="custom-paragraph">
  <style>
    p {
      color: white;
      background-color: #666;
      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-paragraph> 元素内包含一个 HTML 结构,并使用 slot 属性,其值等于我们希望它填充的插槽的名称。和以前一样,这可以是任何您喜欢的,例如

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

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

注意:可以插入插槽的节点称为可插槽节点;当节点已插入插槽时,则称其为已插槽

注意:未命名的 <slot> 将填充所有不具有 slot 属性的自定义元素的顶级子节点。这包括文本节点。

我们的简单示例到此结束。如果您想进一步尝试,可以在 GitHub 上找到它(也可以查看 运行中的示例)。

更复杂的示例

为了完成本文,让我们看一个不太简单的示例。

以下代码片段集展示了如何将 <slot><template> 和一些 JavaScript 一起使用,以

  • 创建一个具有 命名插槽<element-details> 元素,其 Shadow Root
  • 设计 <element-details> 元素,以便在文档中使用时,通过将元素的内容与其 Shadow Root 的内容组合在一起进行渲染,也就是说,元素内容的部分用于填充其 Shadow Root 中的 命名插槽

请注意,从技术上讲,可以使用 <slot> 元素而无需 <template> 元素,例如,在普通的 <div> 元素中,仍然可以利用 <slot> 的占位符功能用于 Shadow DOM 内容,这样做确实可以避免需要首先访问模板元素的 content 属性(并克隆它)的小麻烦。但是,通常更实用的是在 <template> 元素中添加插槽,因为您不太可能需要根据已渲染的元素定义模式。

此外,即使它尚未渲染,当使用 <template> 时,容器作为模板的目的在语义上应该更加清晰。此外,<template> 可以直接添加项目,如 <td>,这些项目在添加到 <div> 时将消失。

注意:您可以在 element-details 中找到此完整示例(也可以查看 运行中的示例)。

创建带有一些插槽的模板

首先,我们在 <template> 元素中使用 <slot> 元素来创建一个新的“element-details-template” 文档片段,其中包含一些 命名插槽

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 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 将其附加到该元素上,作为其 Shadow Root,即我们使用上面的 <template> 元素创建的文档片段。这使用了与我们之前在简单示例中看到的完全相同的模式。

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> 元素使用一个 <dl> 元素引用 "attributes" 命名插槽,该元素具有 <dt><dd> 子元素。

添加最后一点样式

作为最后的润色,我们将为文档中的 <dl><dt><dd> 元素添加少量 CSS

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

结果

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

请注意关于此渲染结果的以下几点

  • 即使文档中的 <element-details> 元素实例没有直接使用 <details> 元素,它们也会使用 <details> 进行渲染,因为 Shadow Root 会导致它们被填充。

  • 在渲染后的 <details> 输出中,<element-details> 元素中的内容填充了来自 阴影根命名插槽。换句话说,来自 <element-details> 元素的 DOM 树与 阴影根 的内容组合在一起。
  • 对于两个 <element-details> 元素,来自 阴影根 的“属性”标题会自动添加到 "attributes" 命名插槽 位置之前。
  • 因为第一个 <element-details> 包含一个 <dl> 元素,该元素明确引用了其 阴影根 中的 "attributes" 命名插槽,所以该 <dl> 元素的内容替换了来自 阴影根"attributes" 命名插槽
  • 因为第二个 <element-details> 没有明确引用其 阴影根 中的 "attributes" 命名插槽,所以该命名插槽的内容将填充来自 阴影根 的默认内容。