DevGang
Авторизоваться

CSS nth- переменная селектора

Использование переменных CSS, по крайней мере, когда я пишу эти строки в июне 2021 года, не поддерживается в медиа-запросах или селекторе, например :nth-child(var(--my-variable)), не работает.

Это немного прискорбно, но не является неразрешимым. В некоторых недавних разработках я обошел это ограничение, вставив элементы style в DOM в свои веб-компоненты, чтобы оживить блок кодов в DeckDeckGo.

Вступление

Строго говоря, следующий трюк не предназначен только для веб-компонентов и, вероятно, также работает с любыми элементами. Я просто до сих пор использовал его только с такой технологией 😜.

Сначала я продемонстрирую идею с помощью vanilla компонента и завершу статью тем же подходом, но реализованным с помощью функционального компонента StencilJS.

Цель учебного пособия

Мы собираемся разработать веб-компонент, который отображает список <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 для отображения и создает CSS selector для применения стиля opacity. Короче говоря, это заменяет переменную CSS, которую мы не можем использовать.

После этого мы проверяем, содержит ли уже затененный контент нашего веб-компонента специальный стиль для применения анимации. Если это так, мы обновляем стиль новым значением - selector, а если нет, мы создаем новый тег стиля.

Каждый раз, когда вызывается этот метод, применяется новый style и, следовательно, отображается еще один li:nth-child(n).

Если мы снова откроем наш браузер, чтобы попробовать, элементы должны быть анимированы при нажатии на нашу кнопку next, и, если мы пойдем дальше и понаблюдаем за компонентом в инспекторе, мы должны заметить, что затененный элемент style изменяется при каждом вызове метода.

StencilJS

Давайте удвоим удовольствие на том же примере, но с использованием функционального компонента StencilJS 🤙.

Вы можете начать новый проект из командной строки npm init stencil

Поскольку мы разрабатываем один и тот же компонент, мы можем скопировать предыдущий HTML-контент (объявив компонент и добавив  button) в проект ./src/index.html с незначительной разницей, метод 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.

Бесконечность не предел

Источник:

#JavaScript #CSS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

В подарок 100$ на счет при регистрации

Получить