Как создать компонент списка с помощью 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> с одним и тем же компонентом. Необходима также версия, которая позволяла бы помещать значок в каждый элемент списка. А именно:

Крутая (а также немного странная) возможность в 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 и попытками сделать наши стили более удобочитаемыми.

Пожалуйста, оставьте свои отзывы по текущей теме статьи. Мы очень благодарим вас за ваши комментарии, отклики, подписки, лайки, дизлайки!

Данная публикация является переводом статьи «How to Make a List Component with Emotion» , подготовленная редакцией проекта.

Меню