是否可以以编程方式在 Web 组件中插入元素?

Is it possible to programmatically slot elements in web components?

是否可以自动或以编程方式插入特定类型的嵌套 Web 组件或元素,而无需在其上指定 slot 属性?

考虑这样的结构:

<parent-element>
  <child-element>Child 1</child-element>
  <child-element>Child 2</child-element>

  <p>Content</p>
</parent-element>

<parent-element> 有这样的阴影 DOM:

<div id="child-elements">
  <slot name="child-elements">
    <child-element>Default child</child-element>
  </slot>
</div>
<div id="content">
  <slot></slot>
</div>

预期结果是:

<parent-element>
  <#shadow-root>
    <div id="child-elements">
      <slot name="child-elements">
        <child-element>Child 1</child-element>
        <child-element>Child 2</child-element>
      </slot>
    </div>
    <div id="content">
      <slot>
        <p>Content</p>
      </slot>
    </div>
</parent-element>

换句话说,我想强制执行 <child-element> 只允许在 <parent-element> 内,类似于 <td> 元素只允许在 <tr> 元素内。我希望将它们放在 <slot name="child-elements"> 元素中。必须在它们中的每一个上指定一个 slot 属性以将它们放置在 <parent-element> 的特定插槽中似乎是多余的。 同时,<parent-element> 中的其余内容应自动插入第二个 <slot> 元素。

我在注册父元素时首先搜索了一种定义它的方法,尽管 CustomElementRegistry.define() 目前仅支持 extends 作为选项。

然后我想,也许有一个允许手动插入元素的功能,即类似 childElement.slot('child-elements') 的功能,但似乎不存在。

然后我尝试在父元素的构造函数中以编程方式实现此目的,如下所示:

constructor() {
  super();

  this.attachShadow({mode: 'open'});
  this.shadowRoot.appendChild(template.content.cloneNode(true));

  const childElements = this.getElementsByTagName('child-element');
  const childElementSlot = this.shadowRoot.querySelector('[name="child-elements"]');
  for (let i = 0; i < childElements.length; i++) {
    childElementSlot.appendChild(childElements[i]);
  }
}

尽管这不会将子元素移动到 <slot name="child-elements">,因此所有子元素仍会被放置在第二个 <slot> 元素中。

您的 unnamed 默认值 <slot></slot> 将捕获所有未分配给 named 插槽的元素;
所以 slotchange 事件可以捕获这些事件并强制 child-element 进入正确的位置:

customElements.define('parent-element', class extends HTMLElement {
    constructor() {
      super().attachShadow({mode:'open'})
             .append(document.getElementById(this.nodeName).content.cloneNode(true));
      this.shadowRoot.addEventListener("slotchange", (evt) => {
        if (evt.target.name == "") {// <slot></slot> captures
          [...evt.target.assignedElements()]
            .filter(el => el.nodeName == 'CHILD-ELEMENT') //process child-elements
            .map(el => el.slot = "child-elements"); // force them to their own slot
        } else console.log(`SLOT: ${evt.target.name} got:`,evt.target.assignedNodes())
      })}});
customElements.define('child-element', class extends HTMLElement {
    connectedCallback(parent = this.closest("parent-element")) {
      // or check and force slot name here
      if (this.parentNode != parent) {
        if (parent) parent.append(this); // Child 3 !!!
        else console.error(this.innerHTML, "wants a PARENT-ELEMENT!");
      }}});
child-element { color: red; display: block; } /* style lightDOM in global CSS! */
<template id=PARENT-ELEMENT>
  <style>
    :host { display: inline-block; border: 2px solid red; }
    ::slotted(child-element) { background: lightgreen }
    div { border:3px dashed rebeccapurple }
  </style>
  <div><slot name=child-elements></slot></div>
  <slot></slot>
</template>

<parent-element>
  <child-element>Child 1</child-element>
  <child-element>Child 2</child-element>
  <b>Content</b>
  <div><child-element>Child 3 !!!</child-element></div>
</parent-element>
<child-element>Child 4 !!!</child-element>

请注意处理逻辑 <child-element> 不是 <parent-element> 直接 子级,您可能想根据自己的需要重写此代码