Wie man eine Listenkomponente mit Emotion erstellt

Avatar of Robin Rendle
Robin Rendle am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Ich habe diese Woche bei Sentry einiges refaktoriert und festgestellt, dass wir keine generische Listenkomponente hatten, die wir projekt- und funktionsübergreifend verwenden konnten. Also habe ich eine angefangen, aber hier ist der Haken: Wir stylen Dinge bei Sentry mit Emotion, womit ich nur oberflächliche Erfahrungen habe und das in der Dokumentation wie folgt beschrieben wird…

[…] eine Bibliothek, die für das Schreiben von CSS-Stilen mit JavaScript entwickelt wurde. Sie bietet eine leistungsstarke und vorhersehbare Stilkomposition sowie eine großartige Entwicklererfahrung mit Funktionen wie Source Maps, Labels und Test-Utilities. Sowohl Zeichenketten- als auch Objektstile werden unterstützt.

Wenn Sie noch nie von Emotion gehört haben, ist die allgemeine Idee folgende: Wenn wir an großen Codebasen mit vielen Komponenten arbeiten, wollen wir sicherstellen, dass wir die Kaskadierung unseres CSS kontrollieren können. Nehmen wir also an, Sie haben eine .active-Klasse in einer Datei und möchten sicherstellen, dass diese nicht die Stile einer völlig separaten Komponente in einer anderen Datei beeinflusst, die ebenfalls eine Klasse von .active hat.

Emotion geht dieses Problem an, indem es benutzerdefinierte Zeichenketten zu Ihren Klassennamen hinzufügt, damit diese nicht mit anderen Komponenten kollidieren. Hier ist ein Beispiel für das HTML, das es ausgeben könnte

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

Ziemlich raffiniert, oder? Es gibt aber auch viele andere Werkzeuge und Workflows, die etwas sehr Ähnliches tun, wie zum Beispiel CSS Modules.

Um mit der Erstellung der Komponente zu beginnen, müssen wir zuerst Emotion in unserem Projekt installieren. Ich werde diese Dinge nicht durchgehen, da sie je nach Umgebung und Einrichtung unterschiedlich sind. Aber sobald das erledigt ist, können wir eine neue Komponente wie diese erstellen

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

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

Das sieht für mich ziemlich seltsam aus, denn wir schreiben nicht nur Stile für das <ul>-Element, sondern definieren auch, dass die Komponente ein <ul> rendern soll. Die Kombination von Markup und Stilen an einem Ort fühlt sich seltsam an, aber mir gefällt, wie einfach es ist. Es stört irgendwie mein mentales Modell und die Trennung der Zuständigkeiten zwischen HTML, CSS und JavaScript.

In einer anderen Komponente können wir diese <List> importieren und wie folgt verwenden

import List from 'components/list';

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

Die Stile, die wir unserer Listenkomponente hinzugefügt haben, werden dann in einen Klassennamen wie .oefioaueg umgewandelt und dann zum <ul>-Element hinzugefügt, das wir in der Komponente definiert haben.

Aber wir sind noch nicht fertig! Beim Design der Liste musste ich in der Lage sein, eine <ul> und eine <ol> mit derselben Komponente zu rendern. Außerdem benötigte ich eine Version, die es mir ermöglicht, ein Icon in jedem Listenelement zu platzieren. Genau wie hier

Das Coole (und auch irgendwie *seltsame*) an Emotion ist, dass wir das Attribut as verwenden können, um auszuwählen, welches HTML-Element wir rendern möchten, wenn wir unsere Komponente importieren. Wir können dieses Attribut verwenden, um unsere <ol>-Variante zu erstellen, ohne eine benutzerdefinierte type-Eigenschaft oder etwas Ähnliches erstellen zu müssen. Und das sieht dann genau so aus

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

Das ist nicht nur für mich seltsam, oder? Es ist jedoch super raffiniert, denn es bedeutet, dass wir keine bizarre Logik in der Komponente selbst haben müssen, nur um das Markup zu ändern.

An diesem Punkt fing ich an, darüber nachzudenken, wie die perfekte API für diese Komponente aussehen könnte, denn dann können wir von dort aus zurückarbeiten. Das stelle ich mir vor

<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>

Nachdem ich diese Skizze angefertigt hatte, wusste ich, dass wir zwei Komponenten benötigen würden, zusammen mit der Möglichkeit, Icon-Unterkomponenten innerhalb der <ListItem> zu verschachteln. Wir können so beginnen

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;
  }
`;

Diese eigenartige ol&-Syntax ist, wie wir Emotion mitteilen, dass diese Stile nur auf ein Element angewendet werden, wenn es als <ol> gerendert wird. Es ist oft eine gute Idee, diesem Element einfach einen background: red; hinzuzufügen, um sicherzustellen, dass Ihre Komponente Dinge korrekt rendert.

Als Nächstes kommt unsere Unterkomponente, die <ListItem>. Es ist wichtig zu beachten, dass wir bei Sentry auch TypeScript verwenden. Bevor wir also unsere <ListItem>-Komponente definieren, müssen wir zuerst unsere Props einrichten

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

Jetzt können wir unsere <IconWrapper>-Komponente hinzufügen, die eine <Icon>-Komponente innerhalb der ListItem dimensioniert. Wenn Sie sich an das Beispiel oben erinnern, wollte ich, dass es ungefähr so aussieht

<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>

Diese IconBusiness-Komponente ist eine bereits bestehende Komponente und wir wollen sie in einen Span wrappen, damit wir sie stylen können. Glücklicherweise benötigen wir nur ein kleines bisschen CSS, um das Icon richtig mit dem Text auszurichten, und die <IconWrapper> kann das alles für uns erledigen

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;
`;

Sobald wir das getan haben, können wir schließlich unsere <ListItem>-Komponente darunter hinzufügen, obwohl sie erheblich komplexer ist. Wir müssen die Props hinzufügen, dann können wir den <IconWrapper> darüber rendern, wenn die icon-Prop vorhanden ist, und die darin übergebene Icon-Komponente rendern. Ich habe auch alle Stile unten hinzugefügt, damit Sie sehen können, wie ich diese Varianten style

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;
    }
  }
`;

Und da haben Sie es! Eine relativ einfache <List>-Komponente, die mit Emotion erstellt wurde. Obwohl ich nach dieser Übung immer noch nicht sicher bin, ob mir die Syntax gefällt. Ich schätze, sie macht die einfachen Dinge *wirklich* einfach, aber die mittelgroßen Komponenten *viel* komplizierter, als sie sein sollten. Außerdem kann sie für Neulinge ziemlich verwirrend sein, und das beunruhigt mich ein wenig.

Aber alles ist eine Lernerfahrung, nehme ich an. So oder so bin ich froh, die Gelegenheit gehabt zu haben, an dieser kleinen Komponente zu arbeiten, denn sie hat mir ein paar gute Dinge über TypeScript, React und den Versuch, unsere Stile einigermaßen lesbar zu machen, beigebracht.