在 web 组件中覆盖外部定义的样式

8
我正在学习web components,但不使用任何第三方库,如Polymer。 其中一个主要卖点是Web组件样式与其他地方定义的样式分离,允许在沙盒环境中为组件的影子DOM设置样式。
我遇到的问题是样式如何通过插槽元素级联。由于插槽元素不是影子DOM的一部分,因此它们只能在组件模板内使用::slotted()选择器进行定位。这很好,但几乎无法保证Web组件将在所有上下文中正确显示,因为外部定义的样式也会具有不可战胜的特异性,并应用于插槽元素。 *除了!important
这个问题可以简化为:

customElements.define("my-nav",
  class extends HTMLElement {
    constructor() {
      super();

      const template = document.querySelector("template#my-nav").content;
      this.attachShadow({ mode: "open" })
        .appendChild(template.cloneNode(true));
    }
  }
);
a {
  color: red; /*  >:(  */
}
<template id="my-nav">
  <style>
    .links-container ::slotted(a) {
      color: lime;
      font-weight: bold;
      margin-right: 20px;
    }
  </style>

  <div class="links-container">
    <slot name="links"></slot>
  </div>
</template>

<p>I want these links to be green:</p>
<my-nav>
  <a href="#" slot="links">Link 1</a>
  <a href="#" slot="links">Link 2</a>
  <a href="#" slot="links">Link 3</a>
</my-nav>

我很难理解这个“特性”的价值。我要么必须以其他格式指定我的链接并使用JS创建它们的节点,要么在我的颜色属性中添加!important——但这仍然不能保证我没有定义的任何其他属性的一致性。
这个问题已经得到解决了吗?或者通过更改我的轻量DOM结构来轻松解决?我不确定如何将链接列表放入插槽中。
2个回答

4
<slot>的设计意图是让外部代码可以对其内部内容进行样式设置。当使用得当时,这是一个非常好的特性。
但是如果您想更好地控制 Web 组件中显示的内容,则需要将this.childNodes中的克隆副本复制到 Shadow DOM 中。然后您就可以100%控制 CSS。
好吧,实际上您只有90%的控制权,因为使用您的组件的人仍然可以设置style属性。

customElements.define("my-nav",
  class extends HTMLElement {
    constructor() {
      super();

      const template = document.querySelector("template#my-nav").content;
      this.attachShadow({ mode: "open" })
        .appendChild(template.cloneNode(true));
    }
    
    connectedCallback() {
      var container = this.shadowRoot.querySelector('.links-container');
      var children = this.childNodes;
      if (children.length > 0 && container) {
      
        while(container.firstChild) {
          container.removeChild(container.firstChild);
        }
        
        for (var i = 0; i < children.length; i++) {
          container.appendChild(children[i].cloneNode(true));
        }
      }
    }
  }
);
a {
  color: red;
}
<template id="my-nav">
  <style>
    .links-container a {
      color: lime;
      font-weight: bold;
      margin-right: 20px;
    }
  </style>

  <div class="links-container">
  </div>
</template>

<p>I want these links to be green:</p>
<my-nav>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <a href="#" style="color: red">Link 3</a>
</my-nav>

如上例所示,第三个链接仍然是红色的,因为我们设置了 style 属性。

如果您想避免这种情况发生,那么您需要从内部内容中删除 style 属性。

customElements.define("my-nav",
  class extends HTMLElement {
    constructor() {
      super();

      const template = document.querySelector("template#my-nav").content;
      this.attachShadow({ mode: "open" })
        .appendChild(template.cloneNode(true));
    }
    
    connectedCallback() {
      var container = this.shadowRoot.querySelector('.links-container');
      var children = this.childNodes;
      if (children.length > 0 && container) {
      
        while(container.firstChild) {
          container.removeChild(container.firstChild);
        }
        
        for (var i = 0; i < children.length; i++) {
          container.appendChild(children[i].cloneNode(true));
        }
        
        container.querySelectorAll('[style]').forEach(el => el.removeAttribute('style'));
      }
    }
  }
);
a {
  color: red;
}
<template id="my-nav">
  <style>
    .links-container a {
      color: lime;
      font-weight: bold;
      margin-right: 20px;
    }
  </style>

  <div class="links-container">
  </div>
</template>

<p>I want these links to be green:</p>
<my-nav>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <a href="#" style="color: red">Link 3</a>
</my-nav>

我甚至创建了一些组件,允许唯一的子元素,我读取并转换为自定义内部节点。

想象一下<video>标签及其<source>子元素。这些子元素实际上不会渲染任何内容,它们只是一种保存数据的方式,用于指示要播放的视频源位置。

关键在于理解<slot>应该用于什么,并且只使用它来完成本来的目的,而不试图强制它做一些它从未打算做的事情。

额外加分

由于每次将此节点放入DOM中时都会调用ConnectedCallback,因此您必须小心,每次都要删除阴影DOM中的任何内容,否则您将重复多次子元素。

customElements.define("my-nav",
  class extends HTMLElement {
    constructor() {
      super();

      const template = document.querySelector("template#my-nav").content;
      this.attachShadow({ mode: "open" })
        .appendChild(template.cloneNode(true));
    }
    
    connectedCallback() {
      var container = this.shadowRoot.querySelector('.links-container');
      var children = this.childNodes;
      if (children.length > 0 && container) {
        for (var i = 0; i < children.length; i++) {
          container.appendChild(children[i].cloneNode(true));
        }
      }
    }
  }
);

function reInsert() {
  var el = document.querySelector('my-nav');
  var parent = el.parentNode;
  el.remove();
  parent.appendChild(el);
}

setTimeout(reInsert, 1000);
setTimeout(reInsert, 2000);
a {
  color: red;
}
<template id="my-nav">
  <style>
    .links-container a {
      color: lime;
      font-weight: bold;
      margin-right: 20px;
    }
  </style>

  <div class="links-container">
  </div>
</template>

<p>I want these links to be green:</p>
<my-nav>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <a href="#" style="color: red">Link 3</a>
</my-nav>

因此,删除重复的节点非常重要:

customElements.define("my-nav",
  class extends HTMLElement {
    constructor() {
      super();

      const template = document.querySelector("template#my-nav").content;
      this.attachShadow({ mode: "open" })
        .appendChild(template.cloneNode(true));
    }
    
    connectedCallback() {
      var container = this.shadowRoot.querySelector('.links-container');
      var children = this.childNodes;
      if (children.length > 0 && container) {
        while(container.firstChild) {
          container.removeChild(container.firstChild);
        }
        for (var i = 0; i < children.length; i++) {
          container.appendChild(children[i].cloneNode(true));
        }
      }
    }
  }
);

function reInsert() {
  var el = document.querySelector('my-nav');
  var parent = el.parentNode;
  el.remove();
  parent.appendChild(el);
}

setTimeout(reInsert, 1000);
setTimeout(reInsert, 2000);
a {
  color: red;
}
<template id="my-nav">
  <style>
    .links-container a {
      color: lime;
      font-weight: bold;
      margin-right: 20px;
    }
  </style>

  <div class="links-container">
  </div>
</template>

<p>I want these links to be green:</p>
<my-nav>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <a href="#" style="color: red">Link 3</a>
</my-nav>


哇,这些是我在直接复制到shadow DOM 中时从未考虑过的许多事情,特别是你提到的关于内联样式的观点。我确实试图为<slot>使用一些不该用的东西。非常感谢! - Scott

3

您说得对,除了对每个CSS属性使用!important 之外,没有其他解决方案。

相反,我不会使用<slot>,而是复制您需要的节点:

customElements.define("my-nav",
  class extends HTMLElement {
    constructor() {
      super();

      const template = document.querySelector("template#my-nav").content;
      this.attachShadow({ mode: "open" })
        .appendChild(template.cloneNode(true));
    }
    
    connectedCallback() {
      var links = this.querySelectorAll( 'a[slot]' )
      var container =  this.shadowRoot.querySelector( '.links-container' )
      links.forEach( l => container.appendChild( l ) )
    }
  }
);
a {
  color: red; /*  >:(  */
}
<template id="my-nav">
  <style>
    .links-container > a {
      color: lime;
      font-weight: bold;
      margin-right: 20px;
    }
  </style>

  <div class="links-container">

  </div>
</template>

<p>I want these links to be green:</p>
<my-nav>
  <a href="#" slot="links">Link 1</a>
  <a href="#" slot="links">Link 2</a>
  <a href="#" slot="links">Link 3</a>
</my-nav>


这绝对是最干净的解决方案,实现了我想要的 - 谢谢! - Scott

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接