SVG-UI-Komponenten erstellen 

Avatar of Sarah Drasner
Sarah Drasner am

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

Ich bin fest davon überzeugt, dass SVG eine ganze Welt des Interface-Designs im Web eröffnet. Es mag anfangs einschüchternd wirken, SVG zu lernen, aber es gibt eine Spezifikation, die zum Erstellen von Formen konzipiert wurde und dennoch Elemente wie Text, Links und Aria-Labels bereitstellt. Man kann ähnliche Effekte mit CSS erzielen, aber es ist etwas kniffliger, die Positionierung, insbesondere über verschiedene Ansichten und für responsives Design, perfekt hinzubekommen.

Das Besondere an SVG ist, dass die gesamte Positionierung auf einem Koordinatensystem basiert, ähnlich wie beim Spiel Schiffe versenken. Das bedeutet, dass die Entscheidung, wo alles platziert und wie es gezeichnet wird, sowie wie es sich zueinander verhält, wirklich einfach zu durchdenken ist. CSS-Positionierung ist für das Layout gedacht, was großartig ist, da die Elemente im Fluss des Dokuments miteinander korrespondieren. Dieser sonst positive Aspekt ist jedoch schwieriger zu handhaben, wenn man eine sehr spezifische Komponente mit überlappenden und präzise platzierten Elementen erstellt.

Wirklich, sobald Sie SVG beherrschen, können Sie alles zeichnen und es auf jedem Gerät skalieren lassen. Selbst diese Seite verwendet SVG für benutzerdefinierte UI-Elemente, wie meinen Avatar oben (Meta!).

Der kleine Halbkreis unter dem Autoren-Bild ist nur SVG-Markup.

Wir werden in diesem Beitrag nicht *alles* über SVG behandeln (einige Grundlagen dazu lernen Sie hier, hier, hier und hier), aber um die Möglichkeiten aufzuzeigen, die SVG für die Entwicklung von UI-Komponenten eröffnet, sprechen wir über einen bestimmten Anwendungsfall und analysieren, wie wir über den Aufbau von etwas Benutzerdefiniertem nachdenken würden.

Die Timeline-Task-Listen-Komponente

Vor kurzem habe ich mit meinem Team bei Netlify an einem Projekt gearbeitet. Wir wollten dem Betrachter anzeigen, welches Video in einer Reihe von Videos eines Kurses gerade angesehen wird. Mit anderen Worten, wir wollten so etwas wie eine To-do-Liste erstellen, die jedoch den Gesamtfortschritt anzeigt, wenn Elemente abgeschlossen sind. (Wir haben eine kostenlose Lernplattform im Weltraum-Thema erstellt und sie ist verdammt cool. Ja, ich habe *verdammt* gesagt.)

So sieht das aus

Wie würden wir also vorgehen? Ich zeige ein Beispiel in Vue und React, damit Sie sehen können, wie es in beiden Frameworks funktionieren könnte.

Die Vue-Version

Wir haben uns entschieden, die Plattform in Next.js zu entwickeln, um sie selbst zu testen (d. h. unser eigenes Next on Netlify Build Plugin auszuprobieren), aber da ich besser mit Vue vertraut bin, habe ich den ersten Prototyp in Vue geschrieben und ihn dann nach React portiert.

Hier ist die vollständige CodePen-Demo

Lassen Sie uns diesen Code etwas näher betrachten. Zunächst ist dies eine Single-File-Komponente (SFC), sodass das Template-HTML, das reaktive Skript und die scoped Styles in dieser einen Datei gekapselt sind.

Wir speichern einige Dummy-Aufgaben in data, einschließlich, ob jede Aufgabe erledigt ist oder nicht. Wir erstellen auch eine Methode, die wir über eine Click-Direktive aufrufen können, um den Status auf "erledigt" oder "nicht erledigt" umzuschalten.

<script>
export default {
  data() {
    return {
      tasks: [
        {
          name: 'thing',
          done: false
        },
        // ...
      ]
    };
  },
  methods: {
    selectThis(index) {
      this.tasks[index].done = !this.tasks[index].done
    }
  }
};
</script>

Nun wollen wir ein SVG erstellen, das je nach Anzahl der Elemente eine flexible viewBox hat. Wir wollen auch Screenreadern mitteilen, dass es sich um ein präsentes Element handelt und dass wir einen Titel mit der eindeutigen ID timeline bereitstellen werden. (Weitere Informationen zum Erstellen von zugänglichen SVGs.)

<template>
  <div id="app">
    <div>
      <svg :viewBox="`0 0 30 ${tasks.length * 50}`"
           xmlns="http://www.w3.org/2000/svg" 
           width="30" 
           stroke="currentColor" 
           fill="white"
           aria-labelledby="timeline"
           role="presentation">
           <title id="timeline">timeline element</title>
        <!-- ... -->
      </svg>
    </div>
  </div>
</template>

Die stroke ist auf currentColor eingestellt, um Flexibilität zu ermöglichen – wenn wir die Komponente an mehreren Stellen wiederverwenden möchten, erbt sie die color, die auf dem umschließenden Div verwendet wird.

Als Nächstes wollen wir innerhalb des SVG eine vertikale Linie erstellen, die die Länge der Aufgabenliste hat. Linien sind ziemlich einfach. Wir haben x1 und x2 Werte (wo die Linie auf der x-Achse gezeichnet wird) und ähnlich y1 und y2.

<line x1="10" x2="10" :y1="num2" :y2="tasks.length * num1 - num2" />

Die x-Achse bleibt konstant bei 10, da wir eine Linie nach unten und nicht von links nach rechts zeichnen. Wir speichern zwei Zahlen in data: den Betrag, den wir für den Abstand wünschen, welcher num1 sein wird, und den Betrag, den wir für den Rand wünschen, welcher num2 sein wird.

data() {
  return {
    num1: 32,
    num2: 15,
    // ...
  }
}

Die y-Achse beginnt mit num2, das vom Ende abgezogen wird, sowie dem Rand. Die tasks.length wird mit dem Abstand (num1) multipliziert.

Jetzt benötigen wir die Kreise, die auf der Linie liegen. Jeder Kreis ist ein Indikator dafür, ob eine Aufgabe abgeschlossen wurde oder nicht. Wir benötigen einen Kreis für jede Aufgabe, daher verwenden wir v-for mit einem eindeutigen key, der der Index ist (und hier sicher verwendet werden kann, da sie niemals neu geordnet werden). Wir verbinden die click-Direktive mit unserer Methode und übergeben den Index als Parameter.

Kreise in SVG bestehen aus drei Attributen. Die Mitte des Kreises wird bei cx und cy platziert, und dann zeichnen wir einen Radius mit r. Wie die Linie beginnt cx bei 10. Der Radius ist 4, da dies in diesem Maßstab gut lesbar ist. cy wird wie die Linie beabstandet: Index mal Abstand (num1) plus Rand (num2).

Schließlich verwenden wir einen Ternary-Operator, um die fill-Eigenschaft festzulegen. Wenn die Aufgabe erledigt ist, wird sie mit currentColor gefüllt. Wenn nicht, wird sie mit white (oder was auch immer der Hintergrund ist) gefüllt. Dies könnte beispielsweise mit einer Prop gefüllt werden, die im Hintergrund übergeben wird, wo Sie helle und dunkle Kreise haben.

<circle 
  @click="selectThis(i)" 
  v-for="(task, i) in tasks"
  :key="task.name"
  cx="10"
  r="4"
  :cy="i * num1 + num2"
  :fill="task.done ? 'currentColor' : 'white'"
  class="select"/>

Schließlich verwenden wir CSS Grid, um ein Div mit den Aufgabennamen auszurichten. Dies wird auf ähnliche Weise gestaltet, indem wir durch die Aufgaben iterieren und ebenfalls an dieselbe Klick-Veranstaltung gebunden sind, um den erledigten Status umzuschalten.

<template>
  <div>
    <div 
      @click="selectThis(i)"
      v-for="(task, i) in tasks"
      :key="task.name"
      class="select">
      {{ task.name }}
    </div>
  </div>
</template>

Die React-Version

Hier ist, wo wir mit der React-Version gelandet sind. Wir arbeiten daran, dies Open Source zu machen, damit Sie den vollständigen Code und seine Historie sehen können. Hier sind einige Modifikationen:

  • Wir verwenden CSS-Module anstelle der SFCs in Vue.
  • Wir importieren den Next.js-Link, sodass anstelle des Umschaltens eines "erledigt"-Status die Benutzer zu einer dynamischen Seite in Next.js weitergeleitet werden.
  • Die verwendeten Aufgaben sind tatsächlich Phasen des Kurses – oder "Mission", wie wir sie nennen – die hier übergeben werden, anstatt vom Komponenten gehalten zu werden.

Der Großteil der anderen Funktionalität ist gleich :)

import styles from './MissionTracker.module.css';
import React, { useState } from 'react';
import Link from 'next/link';

function MissionTracker({ currentMission, currentStage, stages }) {
 const [tasks, setTasks] = useState([...stages]);
 const num1 = 32;
 const num2 = 15;

 const updateDoneTasks = (index) => () => {
   let tasksCopy = [...tasks];
   tasksCopy[index].done = !tasksCopy[index].done;
   setTasks(tasksCopy);
 };

 const taskTextStyles = (task) => {
   const baseStyles = `${styles['tracker-select']} ${styles['task-label']}`;

   if (currentStage === task.slug.current) {
     return baseStyles + ` ${styles['is-current-task']}`;
   } else {
     return baseStyles;
   }
 };

 return (
   <div className={styles.container}>
     <section>
       {tasks.map((task, index) => (
         <div
           key={`mt-${task.slug}-${index}`}
           className={taskTextStyles(task)}
         >
           <Link href={`/learn/${currentMission}/${task.slug.current}`}>
             {task.title}
           </Link>
         </div>
       ))}
     </section>

     <section>
       <svg
         viewBox={`0 0 30 ${tasks.length * 50}`}
         className={styles['tracker-svg']}
         xmlns="http://www.w3.org/2000/svg"
         width="30"
         stroke="currentColor"
         fill="white"
         aria-labelledby="timeline"
         role="presentation"
       >
         <title id="timeline">timeline element</title>

         <line x1="10" x2="10" y1={num2} y2={tasks.length * num1 - num2} />
         {tasks.map((task, index) => (
           <circle
             key={`mt-circle-${task.name}-${index}`}
             onClick={updateDoneTasks(index)}
             cx="10"
             r="4"
             cy={index * +num1 + +num2}
             fill={
               task.slug.current === currentStage ? 'currentColor' : 'black'
             }
             className={styles['tracker-select']}
           />
         ))}
       </svg>
     </section>
   </div>
 );
}

export default MissionTracker;

Finale Version

Hier sehen Sie die endgültige, funktionierende Version

Diese Komponente ist flexibel genug, um Listen kleiner und großer Umfänge, verschiedene Browser und responsive Größen zu berücksichtigen. Sie ermöglicht dem Benutzer auch ein besseres Verständnis seines Fortschritts im Kurs.

Aber das ist nur eine Komponente. Sie können beliebig viele UI-Elemente erstellen: Knöpfe, Steuerelemente, Fortschrittsanzeigen, Ladeanimationen… der Fantasie sind keine Grenzen gesetzt. Sie können sie mit CSS oder Inline-Styles gestalten, sie können auf Props, Kontext oder reaktive Daten reagieren, der Fantasie sind keine Grenzen gesetzt! Ich hoffe, das eröffnet Ihnen neue Perspektiven für die Entwicklung von ansprechenderen UI-Elementen für das Web.