样式嵌套 Web 组件,其中 child 可以与 parent 类型相同,但需要不同的样式

Style nested web component where child can be same type as, but needs different styles to, parent

我开发了一个 HTML 网络组件,它有一个插槽,它使用 HTML theme 属性来分隔 child 元素(通过应用 margin 给他们)。我想使用在每个组件实例的 HTML style 属性上设置的 CSS 自定义 属性、--spacing-size 来控制间距的大小。

class VerticalLayout extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: "open"});
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = 
      `<style>
         :host {
           display: flex;
           flex-direction: column;
         }

         :host([theme~="spacing"]) ::slotted(:not(:first-child)) {
           margin-top: var(--spacing-size);
         }
       </style>
       <slot></slot>`;
  }
}

customElements.define("vertical-layout", VerticalLayout);

我 运行 在将我的组件的一个实例添加到另一个实例的槽中时遇到问题,因为我的 CSS 以开槽元素为目标以给它们留余量。因为在这种情况下,开槽元素 我的组件,值 --spacing-size 意味着 its children,它得到的边际意味着它的 children 而不是 parent.

内所需的边际
<vertical-layout theme="spacing" style="--spacing-size: 2em;">
  <div>No margin on this, it is the first child</div>
  <!-- I want a 2em margin on the next element (the size  -->
  <!-- from the layout it sits within), but it gets 0.2em -->
  <!-- because --spacing-size has been redefined -->
  <vertical-layout theme="spacing" style="--spacing-size: 0.2em;">
    <div>No margin on this, it is the first child</div>
    <div>0.2em margin above this</div>
  </vertical-layout>
</vertical-layout>

我创建了一个 codepen。在 codepen 中看到我通过添加第二个自定义 属性、--parent-size 解决了这个问题。 codepen 说明了我在布局上期望的间距,但是 是否有一种巧妙的方法可以仅通过一个自定义实现相同的行为 属性?

在 codepen 中看到,另一个复杂的问题是我明确地将 --spacing-size 设置为 CSS 中的默认值,以便在主题打开但尺寸为未标明。我想这可能会使继承 parent...

中的任何值变得非常困难

我觉得 :host-context() 选择器可能是答案,但我不太明白如何使用它(而且,由于 Safari 不支持它,我将不得不寻找另一个无论如何都是解决方案)。

花了一些时间来完全理解你想要什么(我可能是错的)

  • 您要为所有 CHILDREN(第一个 child 除外)
    指定 margin-top 与:<vertical-layout childmargin="2em">
  • 对于 嵌套 <vertical-layout> 元素应该具有其 PARENT 容器的 margin-top

您的问题:<vertical-layout style="--spacing-size: 2em">,是 2em 设置在 <vertical-layout> 本身(及其全部children)

您希望它仅应用于 children

你不能用 CSS in shadowDOM 做到这一点;因为那没有样式 slotted content.
参见:


我已经更改了您的 HTML 和属性以反映您想要的边距:

(px 表示法以便更好地理解)

    0px <vertical-layout id="Level1" childmargin="15px">
    15px  <div>child1-1</div>
    15px  <div>child1-2</div>
    15px  <div>child1-3</div>
    15px  <vertical-layout id="Level2" childmargin="10px">
    0px    <div>child2-1</div>
    10px   <div>child2-2</div>
    10px   <vertical-layout id="Level3" childmargin="5px">
    5px      <div>child3-1</div>
    5px      <div>child3-2</div>
    5px      <div>child3-3</div>
           </vertical-layout>
    10px    <div>child2-3</div>
         </vertical-layout>
    15px <div>child1-4</div>
    15px <div>child1-5</div>
        </vertical-layout>

CSS不能读取那个childmargin;所以需要 JS 将该值 应用到 child 元素

因为您也不想设置 first-child...

的样式

connectedCallback 的代码是:

    connectedCallback() {
      let margin = this.getAttribute("childmargin");
      setTimeout(() => {
        let children = [...this.querySelectorAll("*:not(:first-child)")];
        children.forEach(child=>child.style.setProperty("--childmargin", margin));
      });
    }

备注

  • * 有点残酷.. 如果你有大量 child 元素,你可能想使用更具体的选择器;也许:
[...this.children].forEach((child,idx)=>{
  if(idx) ....
};
  • 你正在循环所有children;也可以直接在这里设置样式..不需要 CSS then

  • setTimeout 是必需的,因为当 connectedCallback 触发时,所有 child 还没有被解析

因为你所有的 <vertical-layout> 都在 GLOBAL DOM(并得到 反射<slot>个元素)

您在 GLOBAL CSS:

中设置所有样式
  vertical-layout > *:not(:first-child)  {
    margin-top: var(--childmargin);
  }

那么所有需要的 Web 组件代码是:

customElements.define("vertical-layout", class extends HTMLElement {
  constructor() {
    super()
     .attachShadow({mode:"open"})
     .innerHTML = "<style>:host{display:flex;flex-direction:column}</style><slot></slot>";
  }
  connectedCallback() {
    let margin = this.getAttribute("childmargin");
    setTimeout(() => {
        let children = [...this.querySelectorAll("*:not(:first-child)")];
        children.forEach(child=>child.style.setProperty("--childmargin", margin));
      });
  }
});

或者在一个完整的操场上:

https://jsfiddle.net/CustomElementsExamples/sywr8v4f/

<vertical-layout id="Level1" childmargin="15px">
  <div>child1-1</div>
  <div>child1-2</div>
  <div>child1-3</div>
  <vertical-layout id="Level2" childmargin="10px">
    <div>child2-1</div>
    <div>child2-2</div>
    <vertical-layout id="Level3" childmargin="5px">
      <div>child3-1</div>
      <div>child3-2</div>
      <div>child3-3</div>
    </vertical-layout>
    <div>child2-3</div>
  </vertical-layout>
  <div>child1-4</div>
  <div>child1-5</div>
</vertical-layout>

<style>
  body {
    font: 12px arial;
  }
  vertical-layout > *:not(:first-child)  {
    font-weight: bold;
    margin-top: var(--childmargin);
  }
  vertical-layout::before {
    content: "<vertical-layout " attr(id) " childmargin=" attr(childmargin);
  }
  vertical-layout > vertical-layout {
    background: lightblue;
    border-top: 4px dashed red;
  }
  vertical-layout > vertical-layout > vertical-layout {
    background: lightcoral;
  }
</style>

<script>
  customElements.define("vertical-layout", class extends HTMLElement {
    constructor() {
      super()
        .attachShadow({
          mode: "open"
        })
        .innerHTML =
        `<style>
         :host {
           display: flex;
           flex-direction: column;
           background:lightgreen;
           padding-left:20px;
           border:2px solid red;
         }
         ::slotted(*){margin-left:20px}
         :host([childmargin]) ::slotted(:not(:first-child)) {
           color:blue;
         }
       </style>
       &lt;slot>
       <slot></slot>
       &lt;/slot>`;
    }
    connectedCallback() {
      let margin = this.getAttribute("childmargin");
      setTimeout(() => {
        let children = [...this.querySelectorAll("*:not(:first-child)")];
        children.map(child=>{
          child.style.setProperty("--childmargin", margin);
          child.append(` margin-top: ${margin}`);
        })
      });
    }
  });

</script>