嵌套主题组件:根据最近主题 parent 的主题 class 应用样式,忽略更深层次的 parents 轴承主题 classes

Nested themable components: apply styles depending on a theme class of the nearest themed parent, ignoring deeper parents bearing theme classes

我有可以主题化的组件:

<div class="ComponentFoo theme-blue">
</div>

组件可以相互嵌套。

我希望将应用于 parent 组件的主题传播到其所有 children.

一个天真的实现可能是这样的:

<div class="ComponentFoo theme-blue">
  <div class="ComponentBar">
    <div class="ComponentBaz">
    </div>
  </div>

  <div class="ComponentBar">
  </div>

  <div class="ComponentBar">
  </div>
</div>
.theme-blue.ComponentFoo,
.theme-blue .ComponentFoo {
  background-color: blue;
}

.theme-blue.ComponentBar,
.theme-blue .ComponentBar {
  color: white;
}

这在简单的情况下效果很好,但在嵌套主题时会失败:

<div class="ComponentFoo theme-red">
  <div class="ComponentBar theme-blue">
    <div class="ComponentBaz">
    </div>
  </div>
</div>
.theme-blue.ComponentBaz,
.theme-blue .ComponentBaz {
  border-color: blue,
  color: blue,
  background-color: white;
}

.theme-red.ComponentBaz,
.theme-red .ComponentBaz {
  border-color: red,
  color: red,
  background-color: white;
}

在这种情况下,我预计 ComponentBaz 会采用蓝色主题,因为最近的主题 parent 是蓝色的。但事实并非如此!

这是因为 .theme-blue .ComponentBaz.theme-red .ComponentBaz 选择器都匹配 Baz 组件。 CSS不关心嵌套深度

当两个选择器匹配时,重要的是 CSS 代码中声明的顺序:最后一个获胜。


我可以想象通过以下方式解决这个问题:

  1. 利用 > parent 组合子和某些东西来覆盖 CSS 特性,使用极其大量和冗长的选择器,以便 .theme-red > * 胜过 .theme-red > * > *,等等

    我不喜欢这个解决方案,因为它会使 CSS 不可读。

  2. 使用 programming/templating 将 parent 的主题传递到其所有 children:

    <ComponentFoo @theme="red" as |theme|>
      <ComponentBar @theme={{theme}}>
        <ComponentBaz @theme="blue" as |theme2|>
          <ComponentQuux @theme={{theme2}}/>
        </ComponentBaz>
      </ComponentBar>
    </ComponentFoo>
    

    我不喜欢这个解决方案,因为它也很冗长并且引入了太多耦合。

  3. 只需将主题 HTML class 显式应用到每个主题组件。

    这就是我正在做的,但我认为这不是解决方案。更像是一种解决方法,弊端较小。


有哪些更优雅的实现方式?我想要一个纯粹的 CSS 解决方案,它可以让我在 parent 上使用 HTML class,以便将样式应用于 children 并覆盖 grand-parents' 样式。

由于 CSS 非常有限,我们使用 Sass 预处理器。如果使用 Sass.

非常优雅地抽象出来,我不介意使用产生混乱 CSS 的解决方案

我认为您在 CSS 中设置了太多规则。

为什么不只为主题设置选择器,让继承来完成这项工作?

.theme-blue {
  border-color: blue;
  color: blue;
  background-color: white;
}

.theme-red  {
  border-color: red;
  color: red;
  background-color: white;
}

div {
    border-width: 1px;
    border-style: solid;
    padding: 4px;
}
<div class="ComponentFoo theme-red">I am red
  <div class="ComponentBar theme-blue">I am blue
    <div class="ComponentBaz">I am nested
    </div>
  </div>
</div>


<div class="ComponentFoo theme-blue">I am blue
  <div class="ComponentBar theme-red">I am red
    <div class="ComponentBaz">I am nested
    </div>
  </div>
</div>

使用 CSS 常量的替代解决方案

.theme-blue {
  --border-color: blue;
  --color: blue;
  --background-color: white;
}

.theme-red  {
  --border-color: red;
  --color: red;
  --background-color: white;
}

.ComponentFoo, .ComponentBar, .ComponentBaz {
  border-color: var(--border-color);
  color: var(--color);
  background-color: var(--background-color);
}

.other {
  border-color: black;
  color: green;
  background-color: silver;
}

div {
    border-width: 1px;
    border-style: solid;
    padding: 4px;
}
<div class="ComponentFoo theme-red">I am red
  <div class="ComponentBar theme-blue">I am blue
     <div class="other">other
        <div class="ComponentBaz">I am nested
    </div>
    </div>
  </div>
</div>


<div class="ComponentFoo theme-blue">I am blue
  <div class="ComponentBar theme-red">I am red
     <div class="other">other
        <div class="ComponentBaz">I am nested
    </div>
    </div>
  </div>
</div>

当然你可以再添加一个class,"themable",把选择器.ComponentFoo .ComponentBar .ComponentBaz改成".themable"

这是如何工作的:

在 theme-blue class 中定义 CSS 常量(有些人称之为 CSS 变量)的值。此值按照级联规则传播,因此所有子项和后代都将具有相同的值。但是,与所有 CSS 继承一样,如果子项重新声明此值,则该子项的子项将继承重新声明的值。

现在所需要做的就是将标准 CSS 属性设置为此继承值,这是使用 var() 语法完成的

通过你的问题,我认为你想要 select child 而不是盛大的 child。为此,在 CSS 中我们有 > select 或。

例如:

.test_class_1 > .test_class_2

这里会 select child 和 class test_class_2 但不会 select 里面的 grandchild test_class_2 div.

据此,我稍微修改了你的css:

.theme-blue>.ComponentBaz,
.theme-blue>.ComponentBaz {
  border-color: blue;
  color: blue;
  background-color: white;
}

.theme-red>.ComponentBaz,
.theme-red>.ComponentBar {
  border-color: red;
  color: red;
  background-color: white;
}

这里我添加了 > 其中 selects div 和 ComponentBaz 作为 class 但 不是 这个 div 里面的 div。我还将“,”替换为“;”也许那只是打字错误。

这是 JSfiddle link:https://jsfiddle.net/o20dLy7k/

如果您可以将 css 限制为主题 background、组件 colorborder,那么您就走运了。
一切都可以简化为 2 种颜色——在我的片段中,第一张图是蓝色和黑色——只有 3 种微妙的情况,它们都是微妙的,只是因为文本的可见性:

  • 主题light-blue+组件light-blue(或黑黑)
  • 继承light-blue主题+组件light-blue
  • 主题light-blue + 继承组件light-blue

不用担心了。说真的,一切都很好,validator 是绿色的,没有评论。
第一张图 - 2 种颜色 - 对可能的担忧的测试,第二张丑陋但信息丰富 - 各种组合的十几种颜色。我使用 ::before{ content: attr(class)} 作为内容,所以我的 css 看起来比平时差。
我决定,非常、非常复杂的情况——比如在图的中心——不应该修正它们的文本颜色(特殊处理改变了遗留),代码太多。如果你愿意,你可以使用不同于 color 的东西,问题就会消失。
当然,使用的颜色必须是可见的。

function switcher(){
    var element = document.getElementById("body");
    if(element.classList){  element.classList.toggle("shadow");}
    else{
        var classes = element.className.split(" ");
        var i = classes.indexOf("shadow");
        if(i >= 0)  classes.splice(i, 1);
        else    classes.push("shadow");
        element.className = classes.join(" ");}}
body{ color: #000}
p, span, h1, h2, h3, h4, h5, h6, div::before{ background: #fff}
    /* themes */
.shadow .black{ background: #000;   color: #fff}
.black{ background: #000;}
.red{   background: #f00;}
.green{ background: #0d0;}
.blue{  background: #00f}
.white{ background: #fff}
.gold{  background: gold}
.purple{    background: purple}
.grey{  background: grey}
.teal{  background: teal}
.lblue{ background: #5ae}
    /* components */
.Cblack{    border-color: black;        color: black}
.Cred{  border-color: red;      color: red}
.Cgreen{    border-color: green;    color: green}
.Cblue{ border-color: blue;     color: blue}
.Cwhite{    border-color: white;        color: white}
.Cgold{ border-color: gold;     color: gold}
.Cpurple{   border-color: purple;   color: purple}
.Cteal{ border-color: teal;     color: teal}
.Clblue{    border-color: #5ae;     color: #5ae}
.Cwhite::before, .Cwhite>::before{ background: #aaa}
    /* shadow */
.shadow .lblue .Clblue::before, .shadow .red .Cred::before, .shadow .green .Cgreen::before, .shadow .blue .Cblue::before, .shadow .white .Cwithe::before, .shadow .gold .Cgold::before, .shadow .purple .Cpurple::before, .shadow .grey .Cgrey::before, .shadow .teal .Cteal::before, .shadow .black .Cblack::before,
.shadow .Clblue .lblue::before, .shadow .Cred .red::before, .shadow .Cgreen .green::before, .shadow .Cblue .blue::before, .shadow .Cwhite .withe::before, .shadow .Cgold .gold::before, .shadow .Cpurple .purple::before, .shadow .Cgrey .grey::before, .shadow .Cteal .teal::before, .shadow .Cblack .black::before, 
.shadow .lblue.Clblue::before, .shadow .red.Cred::before, .shadow .green.Cgreen::before, .shadow .blue.Cblue::before, .shadow .white.Cwhite::before, .shadow .gold.Cgold::before, .shadow .purple.Cpurple::before, .shadow .grey.Cgrey::before, .shadow .teal.Cteal::before, .shadow .black.Cblack::before{
border: 1px dotted black; 
text-shadow: -1px 0 1px black, 0 1px 1px black, 1px 0 1px black, 0 -1px 1px black;}
.shadow .black .Cblack::before, .shadow .Cblack .black::before, .shadow .black.Cblack::before{
border: 1px dotted white;
text-shadow: -1px 0 1px white, 0 1px 1px white, 1px 0 1px white, 0 -1px 1px white}
.shadow ::before, .shadow p, .shadow span, .shadow h4{ background: 0}
    /* for snippet only */
button{ position: fixed; top: 45vh; right: 0; background: #acf; border: 12px solid white; color: black; padding: 25px; border-radius: 50%; outline: 0}
div{ margin: 10px 8px; padding: 10px 8px; border: 3px solid transparent;}
div::before{ content: attr(class); border-radius: 8px}
p, span, h1, h2, h3, h4, h5, h6, div::before{ padding: 2px 8px;}
<body id="body" class="x">

<div class="lblue Clblue"><h4>h4</h4><p>paragraph</p><span>span</span>
<div class="Cblack">
<div class="black">
<div class="Cblack">
<div class="Clblue">
<div class="lblue">
<div class="Cblack"><h4>h4</h4><p>paragraph</p><span>span</span>
<div class="Clblue"><h4>h4</h4><p>paragraph</p><span>span</span></div>
<div class="black"><h4>h4</h4><p>paragraph</p><span>span</span>
</div></div></div></div></div></div></div></div>

<div class="red Cgold">
<div class="black">
<div class="black Cblack">
<div class="red Cblack">
<div class="green">
<div class="blue ">
<div class="white ">
</div></div></div>
<div class="gold ">
<div class="purple ">
<div class="grey Cred"><h4>h4</h4><p>paragraph</p><span>span</span>
<div class="teal ">
<div class="white Cwhite">
</div></div></div>
<div class="teal Cteal">
<div class="white Cwhite">
<div class="blue">
<div class="black Cteal">
<div class="grey Cred">
</div></div></div>
<div class="Cpurple"><h4>h4</h4><p>paragraph</p><span>span</span>
<div class="Cred"><h4>h4</h4><p>paragraph</p><span>span</span>
<div class="Cgold"><h4>h4</h4><p>paragraph</p><span>span</span>
</div></div></div>
<div class="green">
<div class="white Cgreen">
<div class="red Cred"><h4>h4</h4><p>paragraph</p><span>span</span>
<div class="Cwhite"><h4>h4</h4><p>paragraph</p><span>span</span>
</div></div></div>
</div></div></div>
</div></div></div>
</div></div></div>

<button onclick="switcher()"><b>switch</b></button></body>


补充
如果你能确定,文本将始终在标签内提供,忘记阴影

p, span, h1, h2, h3, h4, h5, h6{ background: #fff}

如果您可以排除太亮的颜色 - 就是这样。如果不是...例外...
我将此添加到代码段中,.Cwhite::before

除外

你需要确定你的意思。如果您指的是传统意义上的主题,这些解决方案将引入一个很难发现的错误!。如果多个规则匹配,则将应用规则之间不共有的所有属性。例如,想象一个黑暗主题改变前景和背景,霓虹灯主题改变前景和调色板。无论顺序如何,背景(来自黑暗)和调色板(来自霓虹)都会应用。

一种解决方案是规范您的主题,使每个主题设置任何主题设置的所有属性。