He estado haciendo un poco de refactorización esta semana en Sentry y me di cuenta de que no teníamos un componente de lista genérico que pudiéramos usar en todos los proyectos y funciones. Entonces, comencé uno, pero aquí está el problema: diseñamos las cosas en Sentry usando Emotion, con lo que solo tengo experiencia pasajera y se describe en los documentos como …
[…] una biblioteca diseñada para escribir estilos CSS con JavaScript. Proporciona una composición de estilo potente y predecible, además de una excelente experiencia para el desarrollador con características como mapas de origen, etiquetas y utilidades de prueba. Se admiten tanto los estilos de cadena como de objeto.
Si nunca ha oído hablar de Emotion, la idea general es la siguiente: cuando trabajamos en grandes bases de código con muchos componentes, queremos asegurarnos de que podemos controlar la cascada de nuestro CSS. Entonces, digamos que tienes un .active
class en un archivo y desea asegurarse de que no afecte los estilos de un componente completamente separado en otro archivo que también tenga una clase de.active
.
Emotion aborda este problema agregando cadenas personalizadas a sus nombres de clase para que no entren en conflicto con otros componentes. Aquí hay un ejemplo del HTML que podría generar:
<div class="css-1tfy8g7-List e13k4qzl9"></div>
Bastante ordenado, ¿eh? Sin embargo, hay muchas otras herramientas y flujos de trabajo que hacen algo muy similar, como los módulos CSS.
Para comenzar a hacer el componente, primero necesitamos instalar Emotion en nuestro proyecto. No voy a explicar todo eso porque será diferente según el entorno y la configuración. Pero una vez que esté completo, podemos seguir adelante y crear un nuevo componente como este:
import React from 'react';
import styled from '@emotion/styled';
export const List = styled('ul')`
list-style: none;
padding: 0;
`;
Esto me parece bastante extraño porque, no solo estamos escribiendo estilos para el <ul>
elemento, pero estamos definiendo que el componente debe representar un <ul>
, también. Combinar tanto el marcado como los estilos en un solo lugar parece extraño, pero me gusta lo simple que es. Simplemente se mete con mi modelo mental y la separación de preocupaciones entre HTML, CSS y JavaScript.
En otro componente, podemos importar este <List>
y utilícelo así:
import List from 'components/list';
<List>This is a list item.</List>
Los estilos que agregamos a nuestro componente de lista se convertirán en un nombre de clase, como .oefioaueg
, y luego agregado al <ul>
elemento que definimos en el componente.
¡Pero aún no hemos terminado! Con el diseño de la lista, necesitaba poder representar un <ul>
y un <ol>
con el mismo componente. También necesitaba una versión que me permitiera colocar un icono dentro de cada elemento de la lista. Así:
Lo bueno (y también un poco extraño) de Emotion es que podemos usar el as
atributo para seleccionar qué elemento HTML nos gustaría representar cuando importamos nuestro componente. Podemos usar este atributo para crear nuestro <ol>
variante sin tener que hacer un personalizado type
propiedad o algo. Y resulta que se ve así:
<List>This will render a ul.</List>
<List as="ol">This will render an ol.</List>
Eso no es solo extraño para mí, ¿verdad? Sin embargo, es genial porque significa que no tenemos que hacer ninguna lógica extraña en el componente en sí solo para cambiar el marcado.
Fue en este punto que comencé a anotar cómo se vería la API perfecta para este componente, porque luego podemos trabajar desde allí. Esto es lo que imaginé:
<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>
Entonces, después de hacer este boceto, sabía que necesitaríamos dos componentes, junto con la capacidad de anidar subcomponentes de íconos dentro del <ListItem>
. Podemos empezar así:
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;
}
`;
Que peculiar ol&
La sintaxis es cómo le decimos a la emoción que estos estilos solo se aplican a un elemento cuando se representa como un <ol>
. A menudo es una buena idea agregar un background: red;
a este elemento para asegurarse de que su componente esté representando las cosas correctamente.
El siguiente es nuestro subcomponente, el <ListItem>
. Es importante tener en cuenta que en Sentry también usamos TypeScript, así que antes de definir nuestro <ListItem>
componente, primero tendremos que configurar nuestros accesorios:
type ListItemProps = {
icon?: React.ReactNode;
children?: string | React.ReactNode;
className?: string;
};
Ahora podemos agregar nuestro <IconWrapper>
componente que dimensionará un <Icon>
componente dentro del ListItem
. Si recuerdas el ejemplo anterior, quería que se viera así:
<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>
Ese IconBusiness
El componente es un componente preexistente y queremos envolverlo en un intervalo para que podamos diseñarlo. Afortunadamente, necesitaremos solo un poquito de CSS para alinear el ícono correctamente con el texto y el <IconWrapper>
puede manejar todo eso por nosotros:
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;
`;
Una vez que hayamos hecho esto, finalmente podemos agregar nuestro <ListItem>
componente debajo de estos dos, aunque es considerablemente más complejo. Necesitaremos agregar los accesorios, luego podemos renderizar el <IconWrapper>
arriba cuando el icon
prop existe, y renderiza el componente de icono que se le ha pasado también. También agregué todos los estilos a continuación para que pueda ver cómo estoy diseñando cada una de estas variantes:
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;
/* Tiny circle and icon positioning */
&:before,
& > ${IconWrapper} {
position: absolute;
left: 0;
}
ul & {
color: #aaa;
/* This pseudo is the tiny circle for ul items */
&: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;
}
/* Removes tiny circle pseudo if icon is present */
&:before {
content: none;
}
`}
}
/* When the list is rendered as an <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;
}
}
`;
¡Y ahí lo tienes! Relativamente simple <List>
componente construido con Emotion. Aunque, después de realizar este ejercicio, todavía no estoy seguro de que me guste la sintaxis. Creo que hace que las cosas simples sean realmente simples, pero los componentes de tamaño mediano mucho más complicados de lo que deberían ser. Además, podría resultar bastante confuso para un recién llegado y eso me preocupa un poco.
Pero todo es una experiencia de aprendizaje, supongo. De cualquier manera, me alegro de haber tenido la oportunidad de trabajar en este pequeño componente porque me enseñó algunas cosas buenas sobre TypeScript, React y tratando de hacer que nuestros estilos sean algo legibles.