Как создать компонент списка с помощью Emotion

На этой неделе я провел небольшой рефакторинг в Sentry и заметил, что в нем нет общего компонента списка, который можно было бы использовать в проектах и ​​функциях. Итак, я начал создавать его, но вот загвоздка: мы стилизуем вещи в Sentry с помощью Emotion, с которым у меня лишь мимолетный опыт и который описан в документации следующим образом:

[…] Библиотека, предназначенная для написания стилей CSS с помощью JavaScript. Она обеспечивает мощную и предсказуемую композицию стилей в дополнение к отличному опыту разработки с такими функциями, как исходные карты, метки и утилиты для тестирования. Поддерживаются как строковые, так и объектные стили.

Если вы никогда не слышали об Emotion, общая идея такова: когда мы работаем над большими базами кода с большим количеством компонентов, мы хотим убедиться, что можем контролировать каскад CSS. Итак, допустим, что у вас в одном файле есть класс .active, и вы хотите убедиться, что он не влияет на стили полностью отдельного компонента, прописанного в другом файле, который также имеет класс .active.

Emotion решает эту проблему, добавляя настраиваемые строки в имена классов, чтобы они не конфликтовали с другими компонентами. Ниже приведен пример HTML-код, который он может вывести.

<div class="css-1tfy8g7-List e13k4qzl9"></div>

Довольно аккуратно, да? Есть много других инструментов и рабочих процессов, которые делают нечто очень похожее, например, CSS-модули.

Шаг 1

Чтобы приступить к созданию компонента, сначала необходимо установить Emotion в проекте. Я не буду вдаваться в подробности, потому что все будет зависеть от используемой среды разработки и настроек. Но когда установка будет завершена, мы сможем создать новый компонент, подобный этому:

import React from 'react';
import styled from '@emotion/styled';

export const List = styled('ul')`
  list-style: none;
  padding: 0;
`;

Мне это кажется довольно странным, потому что мы не только пишем стили для элемента <ul>, но и определяем, что компонент также должен отображать <ul>. Комбинировать разметку и стили в одном месте кажется странным, но мне нравится, насколько это просто. Это противоречит только моей ментальной модели и разделению задач между HTML, CSS и JavaScript.

В другом компоненте мы можем импортировать <List> и использовать его следующим образом:

import List from 'components/list';

<List>This is a list item.</List>

Стили, которые мы добавили в компонент списка, затем будут преобразованы в имя класса, например, .oefioaueg, а затем добавлены к элементу <ul>, который мы определили в компоненте.

Шаг 2

Но мы еще не закончили! Необходимо также иметь возможность визуализировать <ul> и <ol> с одним и тем же компонентом. Необходима также версия, которая позволяла бы помещать значок в каждый элемент списка. А именно:

Шаг 2

Крутая (а также немного странная) возможность в Emotion заключается в том, что мы можем использовать атрибут as, чтобы выбрать, какой HTML-элемент необходимо отобразить при импорте компонента. Мы можем использовать этот атрибут для создания <ol> без необходимости создавать пользовательское свойство type или что-то в этом роде. Все это выглядит примерно так:

<List>This will render a ul.</List>
<List as="ol">This will render an ol.</List>

Это странно, правда? Однако это очень удобно, потому что это означает, что нам не нужно выполнять какую-либо причудливую логику в самом компоненте только для того, чтобы изменить разметку.

Именно в этот момент я начал записывать то, как может выглядеть идеальный API для этого компонента. Вот что я себе представлял:

<List>
  <ListItem>Item 1</ListItem>
  <ListItem>Item 2</ListItem>
  <ListItem>Item 3</ListItem>
</List>

<List>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 1</ListItem>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 2</ListItem>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 3</ListItem>
</List>

<List as="ol">
  <ListItem>Item 1</ListItem>
  <ListItem>Item 2</ListItem>
  <ListItem>Item 3</ListItem>
</List>

Шаг 3

Итак, после создания этого наброска я знал, что нам понадобится два компонента, а также возможность вкладывать подкомпоненты значков в <ListItem>. Начать можно так:

import React from 'react';
import styled from '@emotion/styled';

export const List = styled('ul')`
  list-style: none;
  padding: 0;
  margin-bottom: 20px;

  ol& {
    counter-reset: numberedList;
  }
`;

Этот своеобразный синтаксис ol& - это то, как мы сообщаем Emotion, что эти стили применяются только к элементу, когда он отображается как <ol>. Часто бывает хорошей идеей просто добавить к этому элементу background: red;, чтобы убедиться, что компонент отображается правильно.

Шаг 4

Далее идет подкомпонент <ListItem>. Важно отметить, что в Sentry также используется TypeScript, поэтому, прежде чем мы определим компонент <ListItem>, необходимо настроить свойства:

type ListItemProps = {
  icon?: React.ReactNode;
  children?: string | React.ReactNode;
  className?: string;
};

Теперь мы можем добавить компонент <IconWrapper>, который будет определять размер компонента <Icon> в ListItem. Как вы помните из приведенного выше примера, я хотел, чтобы он выглядел примерно следующим образом:

<List>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 1</ListItem>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 2</ListItem>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 3</ListItem>
</List>

Этот компонент IconBusiness является уже существующим, и мы хотим обернуть его в span, чтобы стилизовать. К счастью, нам понадобится совсем немного CSS, чтобы правильно выровнять значок с текстом, и все это <IconWrapper> может сделать за нас:

type ListItemProps = {
  icon?: React.ReactNode;
  children?: string | React.ReactNode;
  className?: string;
};

const IconWrapper = styled('span')`
  display: flex;
  margin-right: 15px;
  height: 16px;
  align-items: center;
`;

Шаг 5

Когда это сделано, мы наконец-то можем добавить компонент <ListItem> под этими двумя, хотя он значительно сложнее. Нам нужно будет добавить свойства, затем мы сможем отобразить упомянутый выше <IconWrapper>, когда свойство icon существует, и отобразить компонент иконки, который также передан в него. Я также добавил все приведенные ниже стили, чтобы вы могли видеть, как я стилизую каждый из этих вариантов:

export const ListItem = styled(({icon, className, children}: ListItemProps) => (
  <li className={className}>
    {icon && (
      <IconWrapper>
        {icon}
      </IconWrapper>
    )}
    {children}
  </li>
))<ListItemProps>`
  display: flex;
  align-items: center;
  position: relative;
  padding-left: 34px;
  margin-bottom: 20px;
	
  /* Позиционирование маленького кружка и значка */
  &:before,
	& > ${IconWrapper} {
    position: absolute;
    left: 0;
  }

  ul & {
    color: #aaa;
    /* Этот псевдо-элемент – это маленький кружок для элементов ul */ 
    &:before {
      content: '';
      width: 6px;
      height: 6px;
      border-radius: 50%;
      margin-right: 15px;
      border: 1px solid #aaa;
      background-color: transparent;
      left: 5px;
      top: 10px;
    }
		
    /* Icon styles */
    ${p =>
      p.icon &&
      `
      span {
        top: 4px;
      }
      /* Удаляем маленький кружок, если присутствует значок */
      &:before {
        content: none;
      }
    `}
  }
  /* Когда список отображается, как <ol> */
  ol & {
    &:before {
      counter-increment: numberedList;
      content: counter(numberedList);
      top: 3px;
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;
      width: 18px;
      height: 18px;
      font-size: 10px;
      font-weight: 600;
      border: 1px solid #aaa;
      border-radius: 50%;
      background-color: transparent;
      margin-right: 20px;
    }
  }
`;

И вот оно! Относительно простой компонент <List>, созданный с помощью Emotion. Хотя после выполнения этого упражнения я все еще не уверен, что мне нравится синтаксис. Я считаю, что это делает простые вещи действительно простыми, но компоненты среднего размера намного сложнее, чем они должны быть. К тому же, это может быть чертовски запутанным для новичка, и меня это немного беспокоит.

Но я полагаю, что все это требует изучения. В любом случае, я рад, что у меня была возможность поработать над этим крошечным компонентом, потому что он научил меня нескольким хорошим вещам, связанным с TypeScript, React и попытками сделать наши стили более удобочитаемыми.