CSS的第n个选择器变量

338 阅读3分钟

一个实用的黑客,使CSS nth-选择器在Web组件中可变。

使用CSS变量,至少当我在2021年6月写这几行的时候,媒体查询或选择器不支持,例如,:nth-child(var(--my-variable)) 不工作。

这有点不幸,但并不是无法解决的。在最近的开发中,我绕过了这个限制,在DOM中注入了style 元素到Web组件中,以使DeckDeckGo中的代码块成为动画。

简介

严格意义上讲,下面的技巧并不局限于Web组件,可能也适用于任何元素。我只是把它用在这种技术😜。

我将首先用一个普通的组件来展示这个想法,然后用同样的方法来结束这篇文章,但用StencilJS功能组件来实现。

本教程的目标

我们将开发一个Web组件,它可以渲染一个<ul/> 列表,并且可以对其条目进行动画显示。

一旦该组件被加载,将不会从DOM中添加或删除任何语义元素。动画将通过修改style ,更确切地说,是通过在选定的li:nth-child(n) 上应用不同的样式来实现的。

Vanilla JS

为了在没有任何其他东西的情况下显示这个想法,我们创建了一个index.html 页面。它消耗了我们将要开发的Vanilla组件。我们还添加了一个button 来触发动画。

<html>
    <head>
        <script type="module" src="./my-component.js"></script>
    </head>
    <body>
        <my-component></my-component>

        <button>Next</button>

        <script>
            document
              .querySelector('button')
              .addEventListener(
                 'click', 
                 () => document.querySelector('my-component').next()
              );
        </script>
    </body>
</html>

在一个单独的文件中,称为my-component.js ,我们创建了网络组件。在这一点上,没有任何动画。我们声明它是开放的,以便能够访问影子DOM(通过shadowRoot ),我们创建一个样式来隐藏所有li ,并定义transition 。最后,我们添加ul 列表和它的孩子li

class MyComponent extends HTMLElement {

    constructor() {
        super();

        this.attachShadow({mode: 'open'});

        const style = this.initStyle();
        const ul = this.initElement();

        this.shadowRoot.appendChild(style);
        this.shadowRoot.appendChild(ul);
    }

    connectedCallback() {
        this.className = 'hydrated';
    }

    next() {
        // TODO in next chapter
    }

    initStyle() {
        const style = document.createElement('style');

        style.innerHTML = `
          :host {
            display: block;
          }
          
          li {
            opacity: 0;
            transition: opacity 0.5s ease-out;
          }
        `;

        return style;
    }

    initElement() {
        const ul = document.createElement('ul');

        const li1 = document.createElement('li');
        li1.innerHTML = 'Spine';

        const li2 = document.createElement('li');
        li2.innerHTML = 'Cowboy';

        const li3 = document.createElement('li');
        li3.innerHTML = 'Shelving';

        ul.append(li1, li2, li3);

        return ul;
    }
}

customElements.define('my-component', MyComponent);

在这一点上,如果我们在浏览器中打开我们的例子 (npx serve .),我们应该发现一个组件,有一个隐藏的内容,还有一个还没有效果的按钮。这没有什么可看的,但这是一个开始😁。

为了开发动画,我们必须跟踪所显示的li ,这就是为什么我们要给这个组件添加一个状态(index )。

class MyComponent extends HTMLElement {
    index = 0;
    
    constructor() {

由于它的存在,我们可以实现next() 方法,也就是我们先前在HTML页面中添加的按钮所调用的方法。

这不是我最漂亮的代码。让我们同意它只有一个演示的目的😅。

next() {
    this.index = this.index === 3 ? 1 : this.index + 1;

    const selector = `
      li:nth-child(${this.index}) {
        opacity: 1;
      }
    `;

    let style = this.shadowRoot.querySelector('style#animation');

    if (style) {
        style.innerHTML = selector;
        return;
    }

    style = document.createElement('style');
    style.setAttribute('id', 'animation');

    style.innerHTML = selector;

    this.shadowRoot.appendChild(style);
}

那里发生了什么?

它首先设置了下一个indexli ,来显示,并且,创建一个CSSselector 来应用opacity 的样式。简而言之,这取代了我们不能使用的CSS变量。

之后,我们检查我们的Web组件的阴影内容是否已经包含一个专门的样式来应用动画。如果有,我们就用新的值更新样式--选择器,如果没有,我们就创建一个新的样式标签。

每次调用这个方法,都会应用一个新的style ,因此会显示另一个li:nth-child(n)

如果我们再次打开浏览器试一试,在点击我们的按钮next ,项目应该是动画的,而且,如果我们进一步观察检查器中的组件,我们应该注意到阴影的style 元素在每次方法调用时都会改变。

StencilJS

让我们用同样的例子来增加乐趣,但是,使用一个StencilJS功能组件🤙。

你可以用命令行启动一个新的项目npm init stencil

因为我们正在开发完全相同的组件,我们可以在项目的./src/index.html 中复制之前的HTML内容(声明组件并添加一个button ),但只有一个微小的区别,即必须声明方法next() ,并以async - await方式调用。这是一个要求--Stencil的最佳实践,组件的公共方法必须是async

<!DOCTYPE html>
<html dir="ltr" lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
    <title>Stencil Component Starter</title>

    <script type="module" src="/build/demo-stencil.esm.js"></script>
    <script nomodule src="/build/demo-stencil.js"></script>
  </head>
  <body>
  <!-- Same code as in previous chapter -->
  <my-component></my-component>

  <button>Next</button>

  <script>
    document.querySelector('button')
       .addEventListener(
          'click', 
          async () => await document
                             .querySelector('my-component').next()
            );
  </script>
  <!-- Same code as in previous chapter -->
  </body>
</html>

我们也可以重复前面的步骤,先创建一个组件,它只负责渲染一个ul 列表和隐藏的项目li

import { Component, h } from '@stencil/core';

@Component({
  tag: 'my-component',
  styles: `:host {
      display: block;
    }

    li {
      opacity: 0;
      transition: opacity 0.5s ease-out;
    }
  `,
  shadow: true,
})
export class MyComponent {
  render() {
    return <ul>
      <li>Spine</li>
      <li>Cowboy</li>
      <li>Shelving</li>
    </ul>
  }
}

通过测试组件(npm run start ),我们也应该得到同样的结果😉。

为了跟踪要突出显示的li ,我们需要一个状态和,函数state 。我们把这两者都添加到我们的组件中。

@State()
private index: number = 0;

@Method()
async next() {
  this.index = this.index === 3 ? 1 : this.index + 1;
}

与Vanilla组件相比,因为我们使用的是一个简化开发的捆绑器,所以我们不需要自己去处理重新渲染的问题。state 的每一次修改都会触发一次重新渲染,最终更新必须要更新的节点(而且只有那些必须要更新的节点)。

尽管如此,我们还是必须实现CSS选择器变量。为了这个目的,正如简单提到的,我们将使用一个功能组件。它可能与类组件一起工作,但是,我觉得功能组件很适合这项工作。

const Animate: FunctionalComponent<{index: number;}> = ({index}) => {
  return (
    <style>{`
    li:nth-child(${index}) {
      opacity: 1;
    }
  `}</style>
  );
};

这个组件为我们作为参数的路径值渲染了一个style 元素,即我们的state

最后,我们必须使用功能组件,并将其与我们的状态值绑定。这样做,每次它的值发生变化时,它都会被重新渲染。

render() {
  return <Host>
    <Animate index={this.index}></Animate>
    <ul>
      <li>Spine</li>
      <li>Cowboy</li>
      <li>Shelving</li>
    </ul>
  </Host>
}

这已经是它了,我们能够复制同一个组件🥳。

上述组件只用了一个代码块。

import { Component, FunctionalComponent, h, Host, Method, State } from '@stencil/core';

const Animate: FunctionalComponent<{index: number;}> = ({index}) => {
  return (
    <style>{`
    li:nth-child(${index}) {
      opacity: 1;
    }
  `}</style>
  );
};

@Component({
  tag: 'my-component',
  styles: `:host {
      display: block;
    }

    li {
      opacity: 0;
      transition: opacity 0.5s ease-out;
    }
  `,
  shadow: true,
})
export class MyComponent {

  @State()
  private index: number = 0;

  @Method()
  async next() {
    this.index = this.index === 3 ? 1 : this.index + 1;
  }

  render() {
    return <Host>
      <Animate index={this.index}></Animate>
      <ul>
        <li>Spine</li>
        <li>Cowboy</li>
        <li>Shelving</li>
      </ul>
    </Host>
  }
}

总结

说实话,我不确定这篇文章会不会找到它的读者,也不认为它可能在某一天对某人有用,但是,我喜欢用这招😜。此外,用Vanilla JS或Stencil来开发同一段代码的演示也很有趣。