От автора: сегодня рассмотрим, как делается стилизация CSS-in-JS по отношению к пользовательским элементам. Популярность CSS-in-JS в основном исходит от сообщества React, и действительно многие библиотеки CSS-in-JS специфичны для React. Тем не менее, Emotion, самая популярная библиотека с точки зрения количества загрузок через npm, не зависит от фреймворка.
При создании пользовательских элементов распространено использование shadow DOM, но это не обязательно. Не во всех случаях требуется такой уровень инкапсуляции. Хотя также возможно стилизовать пользовательские элементы с помощью CSS в обычной таблице стилей, мы рассмотрим использование Emotion. Начнем с установки:
1 |
npm i emotion |
Emotion предлагает функцию css:
1 |
import {css} from 'emotion'; |
css помечен как литерал шаблона. Он принимает стандартный синтаксис CSS, но добавляет поддержку вложенности в стиле Sass.
1 2 3 4 5 6 7 8 9 |
const buttonStyles = css` color: white; font-size: 16px; background-color: blue; &:hover { background-color: purple; } ` |
После определения некоторых стилей, они должны быть применены. Работа с пользовательскими элементами может быть несколько громоздкой. Такие библиотеки, как Stencil и LitElement — компилируются в веб-компоненты, но предлагают более дружественный API, чем тот, который мы получили бы прямо из коробки.
Итак, мы собираемся определить стили с помощью Emotion и использовать преимущества как Stencil, так и LitElement, чтобы немного упростить работу с веб-компонентами.
Применение стилей для Stencil
Stencil использует новейшую функцию декораторов JavaScript. Декоратор @Component используется для предоставления метаданных о компоненте. По умолчанию Stencil не использует shadow DOM, но я хотел бы явно установить shadow: false внутри декоратора @Component:
1 2 3 4 |
@Component({ tag: 'fancy-button', shadow: false }) |
Stencil использует JSX, поэтому стили применяются с помощью синтаксиса фигурных скобок ({}):
1 2 3 4 5 |
export class Button { render() { return <div><button class={buttonStyles}><slot/></button></div> } } |
Вот как пример простого компонента будет выглядеть в Stencil:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { css, injectGlobal } from 'emotion'; import {Component} from '@stencil/core'; const buttonStyles = css` color: white; font-size: 16px; background-color: blue; &:hover { background-color: purple; } ` @Component({ tag: 'fancy-button', shadow: false }) export class Button { render() { return <div><button class={buttonStyles}><slot/></button></div> } } |
Применение стилей для LitElement
С другой стороны LitElement использует теневой DOM по умолчанию. При создании пользовательского элемента с помощью LitElement расширяется класс LitElement. LitElement содержит метод createRenderRoot(), который создает и открывает shadow DOM:
1 2 3 |
createRenderRoot() { return this.attachShadow({mode: 'open'}); } |
Не хотите использовать shadow DOM? Для этого нужна повторная реализация данного метода внутри класса компонента:
1 2 3 4 5 |
class Button extends LitElement { createRenderRoot() { return this; } } |
Внутри функции render мы можем ссылаться на стили, которые определили, используя литерал шаблона:
1 2 3 |
render() { return html`<button class=${buttonStyles}>hello world!</button>` } |
Стоит отметить, что при использовании LitElement мы можем использовать элемент slot только с shadow DOM (у Stencil такой проблемы нет). Таким образом, мы получаем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import {LitElement, html} from 'lit-element'; import {css, injectGlobal} from 'emotion'; const buttonStyles = css` color: white; font-size: 16px; background-color: blue; &:hover { background-color: purple; } ` class Button extends LitElement { createRenderRoot() { return this; } render() { return html`<button class=${buttonStyles}>hello world!</button>` } } customElements.define('fancy-button', Button); |
Подробнее об Emotion
Нам не нужно беспокоиться об именовании кнопки — Emotion сгенерирует случайное имя класса. Мы могли бы использовать вложенность CSS и прикрепить класс только к родительскому элементу. В качестве альтернативы мы можем определить стили как отдельные тегированные литералы шаблона:
1 2 3 4 5 6 7 8 |
const styles = { heading: css` font-size: 24px; `, para: css` color: pink; ` } |
А затем применить их отдельно к различным элементам HTML (в этом примере используется JSX):
1 2 3 4 5 6 |
render() { return <div> <h2 class={styles.heading}>lorem ipsum</h2> <p class={styles.para}>lorem ipsum</p> </div> } |
Стилизация контейнера
Пока что мы стилизовали внутреннее содержимое пользовательского элемента. Для стилизации самого контейнера нам понадобится еще один импорт из Emotion.
1 |
import {css, injectGlobal} from 'emotion'; |
injectGlobal включает стили в «глобальную область» (например, пишет обычный CSS в традиционной таблице стилей, а не генерирует случайное имя класса). Пользовательские элементы имеют по умолчанию display: inline (несколько странное решение от авторов спецификации). Почти во всех случаях я изменяю это значение по умолчанию, применяя стиль ко всем экземплярам компонента. Ниже приведен buttonStyles, который демонстрирует, как мы можем это изменить, используя injectGlobal:
1 2 3 4 5 |
injectGlobal` fancy-button { display: block; } ` |
Почему бы просто не использовать shadow DOM?
Если компонент может оказаться в любой базе кода, то shadow DOM вполне может быть хорошим вариантом. Он идеально подходит для сторонних виджетов — любой CSS, примененный к странице, просто не сломает компонент благодаря изолированной природе shadow DOM. Вот почему он используется встраиваемыми элементами Twitter. Тем не менее, подавляющее большинство из нас создают компоненты для определенного сайта или приложения и ни для чего больше. В этой ситуации, shadow DOM может увеличить сложность, а предполагаемые преимущества будут ограничены.
Автор: Ollie Williams
Источник: //css-tricks.com
Редакция: Команда webformyself.