Как создать компонент списка с помощью 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 и попытками сделать наши стили более удобочитаемыми.