Startseite / Artikel / Wo die Rahmengrenze in einem wiederverwendbaren UI-Stack platziert werden sollte

Wo die Rahmengrenze in einem wiederverwendbaren UI-Stack platziert werden sollte

Erfahren Sie, wie Zustandsmaschinen, Web Components sowie layout- und bewegungsbasierte Ansätze es ermöglichen, dass das Verhalten einer Benutzeroberfläche über ein Framework hinaus Bestand hat, und wann diese Portabilität nicht lohnenswert ist.

3630 Wörter

Die meisten Teams lösen das Problem der Benutzeroberfläche sehr gut für genau ein Framework. Ein sorgfältig erstelltes Angular-Component-Kit, eine Sammlung von React-Primitiven sowie ein Design-System, das in den Lebenszyklus eines Renderers integriert ist – all das funktioniert hervorragend, bis ein Projekt ein anderes Tool benötigt, und dann nützt fast nichts von dieser Investition mehr. Die Aufteilung der wiederverwendbaren Benutzeroberfläche in Schichten (Verhalten, renderierte Komponenten sowie kleine HTML-Funktionen wie Layout und Animation) ermöglicht es, bewusst zu entscheiden, welche Schichten an ein Framework gebunden werden sollen und welche nicht.

Das Problem bei der Lösung von Benutzeroberflächen für ein einzelnes Framework

Stellen Sie sich ein Frontend-Team vor, das den größten Teil seiner Zeit mit Angular verbringt und die Entwicklung des Frameworks schätzt: Signale, moderne APIs sowie ausreichende Struktur für Anwendungen, die sie benötigen. Ihr internes Werkzeugset folgt dem shadcn-Modell, bei dem die Quelldateien der Komponenten in jedes Projekt kopiert und dort verwaltet werden, wobei für die Arbeit auf niedriger Ebene Angular-spezifische headless-Primitiven genutzt werden. In einer Angular-Anwendung ist das eine hervorragende Einrichtung.

Das Problem beginnt, wenn das nächste Projekt kein Angular-Projekt ist. Eine große, zustandsbasierte Anwendung mit vielen Interaktionen eignet sich möglicherweise hervorragend für Angular. Eine überwiegend statische Marketingseite wird oft besser von Astro bedient. Manchmal bietet bereits die Browser-Plattform fast alles, was benötigt wird. Wenn die Technologie den Anforderungen jedes Projekts folgen soll und nicht umgekehrt, wird eine weitere Frage unvermeidlich:

Wie viel von Ihrer UI-Infrastruktur sollte erhalten bleiben, wenn sich das Framework ändert?

Die Verfolgung dieser Frage führt in der Regel durch eine Abfolge von Ideen: Zunächst Web Components, dann headless UI, anschließend Zustandsmaschinen – und schließlich ein weniger konventionelles Experiment, bei dem Layout und Bewegung eigene HTML-Attribute erhalten. Auf den ersten Blick scheinen sie unzusammenhängend zu sein. Betrachtet man sie gemeinsam, erweisen sie sich als unterschiedliche Lösungen für ein und dasselbe Designproblem: Bis zu welchem Grad kann die Benutzeroberfläche geteilt werden, bevor das Anwendungsframework in jede Abstraktion eindringen muss?

Headless bedeutet nicht framework-unabhängig

Headless UI löst bereits einen großen Teil des Problems. Radix Primitives ist ein klares Beispiel dafür, warum dieses Modell an Bedeutung gewonnen hat. Anstatt ein Dialogfeld mit der Hintergrundfarbe, dem Abstand, dem Schatten und der Randradien eines anderen Anbieters zu liefern, werden die komplexeren Bestandteile bereitgestellt und das Erscheinungsbild bleibt Ihnen überlassen.

Diese schwierigen Aspekte erfordern echte Anstrengung: Fokomanagement, Navigation per Tastatur, korrekte ARIA-Attribute, Verhalten bei Schließen sowie eine Fülle von Details, die leicht übersehen werden können, solange ein Modal noch wie nichts anderes als ein div, der auf einem anderen liegt, erscheint. Radix stellt seine Primitiven absichtlich ungestylt bereit und betrachtet sie als zugängliche React-Primitiven. Sie kontrollieren die Darstellung, doch das darunterliegende Komponentenmodell bleibt weiterhin React.

Das klingt offensichtlich, hat aber Konsequenzen. Die Beseitigung der Abhängigkeit von Stylesheets beseitigt nicht die Abhängigkeit vom Framework. Dasselbe gilt für Angular: Eine Primitive kann völlig neutral gegenüber Farben, Abständen und Typografie sein, während sie vollständig von Angular-Direktiven, Signalen, Dependency Injection und Lifecycle-Hooks abhängt.

Das ist kein Mangel. Oft ist es gerade die richtige Entscheidung. Ein für Angular konzipiertes Primitiv kann sich eng mit Angular integrieren, und ein React-Primitiv kann das Kompositionsmodell von React nutzen. Eine tiefe Integration führt in der Regel zu einer besseren Entwicklererfahrung als das Vortäuschen, dass das Framework nicht vorhanden ist. Der Punkt ist nur, dass zwei Konzepte häufig als Synonyme angesehen werden, obwohl dem nicht so ist:

headless
   ≠
framework-agnostic

Headless-Komponenten eliminieren die visuelle Komponente. Framework-unabhängige Abstraktionen gehen noch einen Schritt weiter und versuchen, Annahmen über den Renderer selbst zu beseitigen. Es handelt sich dabei um zwei unterschiedliche Ebenen der Wiederverwendung, und es ist hilfreich zu wissen, welche man tatsächlich benötigt.

Das Verhalten kann unterhalb der Komponente liegen

Nehmen wir ein Dropdown. Zwei Dropdowns können visuell völlig unterschiedlich aussehen. Eines befindet sich auf einer Marketingseite mit großen Schriftarten, viel Freiraum und animierten Übergängen; das andere existiert in einer engen IDE-Toolleiste, wo jeder Pixel zählt. Eines könnte von Angular, ein anderes von React und ein drittes von einem Web Component rendernt werden.

Hinter den visuellen Unterschieden tauchen immer wieder dieselben Fragen auf:

  • Ist das Dropdown derzeit geöffnet?
  • Welcher Eintrag ist aktiv?
  • Was bewirkt die Taste Escape?
  • Kann der Benutzer mit den Pfeiltasten zwischen den Einträgen wechseln?
  • Wie werden deaktivierte Einträge behandelt?
  • Wohin geht der Fokus, sobald das Dropdown geschlossen wird?

Niemand dieser Fragen hängt davon ab, ob Angular oder React die Markup-Struktur erzeugt hat. Es handelt sich in erster Linie um Interaktionsprobleme und erst in zweiter Linie um Rendering-Probleme.

Zustandsmaschinen als gemeinsamer Vertrag

Genau hier wird eine durch Zustandsmaschinen gesteuerte Benutzeroberfläche besonders attraktiv. Zag.js ist das am weitesten bekannte Beispiel für diesen Ansatz. Anstatt die React-Komponente als Quelle der Wahrheit zu betrachten, modelliert Zag die Interaktion als framework-unabhängige Maschinen. Getrennte Framework-Adapter verbinden anschließend diese Maschinen mit der Art und Weise, wie React, Vue, Solid, Svelte und andere Frameworks Reaktivität, Lebenszyklus und das DOM handhaben, wobei die Dokumentation erklärt, wie man einen Adapter für ein Framework schreibt, das noch nicht abgedeckt wird.

In einer herkömmlichen Komponente ist jede Aufgabe in einer framework-spezifischen Einheit gebündelt:

React Dropdown
  ├─ state
  ├─ interactions
  ├─ accessibility
  └─ rendering

Durch die Zwischenschaltung einer Maschine wird das Verhalten einmal definiert, und jeder Renderer nutzt es anschließend:

Dropdown behavior
                    │
               state machine
                    │
       ┌────────────┼────────────┐
       ↓            ↓            ↓
     React         Vue         Svelte

Die endgültigen Komponenten sind weiterhin getrennte Komponenten, und jedes Framework kann weiterhin wie ein Framework funktionieren. Was verschoben wurde, ist der Verhaltensvertrag – eine nützlichere Definition für framework-unabhängige Ansätze als die Vorstellung von einer magischen Komponente, die überall ohne jegliche Integrationsarbeiten läuft. Eine bessere Zusammenfassung lautet: schreiben Sie das Verhalten einmal und passen Sie es anschließend dem für die Anwendung geeigneten Renderer an.

Ein nützliches mentales Modell ist, dass die Maschine eine reine Beschreibung von Zuständen, Ereignissen und Übergängen darstellt. Da sie weder DOM-Referenzen noch Framework-Zustände enthält, lässt sie sich auch leicht isoliert unitgetestet werden: Man sendet ihr Ereignisse und überprüft den resultierenden Zustand, ohne etwas zu initialisieren.

Eine Komponente kann bereits als Verhalten existieren, bevor sie als UI vorhanden ist

Dieselbe Idee gilt auch für Bibliotheken von Web Components. Stellen Sie sich eine mit Stencil erstellte Bibliothek vor, die die üblichen Schaltflächen, Dialoge, Dropdowns und Karten enthält. Diese Komponenten müssen nicht durch Zustandsmaschinen ersetzt werden; stattdessen kann eine separate Schicht darunter liegen.

Eine Schaltflächenfabrik kennt die Zustände „deaktiviert“ und „lädt“, die Handhabung von Klicks sowie welche Eigenschaften schließlich auf das interaktive Element übertragen werden sollen. Eine Dropdown-Fabrik kennt Öffnen, Schließen, Auswahl sowie die Navigation per Tastatur. Eine Dialog-Fabrik kennt ihren Lebenszyklus und die Interaktionsregeln. Keine dieser Logiken muss entscheiden, wie das Komponentendesign aussieht.

Konzeptionell kann so etwas bereits vor jeder Renderentscheidung existieren:

const button = createButton({
  disabled: false,
  loading: false,
  onClick(event) {
    // application behavior
  },
});

Beachten Sie, was fehlt: kein Stencil, kein Angular, keine React-Komponente – nicht einmal CSS. Die Factory ist lediglich eine Beschreibung des Verhaltens, und ein Renderer kann später hinzugefügt werden. Das Stencil-Button-Element der Bibliothek nutzt beispielsweise dieses Verhalten und stellt ein gestaltetes Custom Element bereit:

<and-button variant="destructive">
  Delete
</and-button>

Eine andere Anwendung kann denselben Verhaltenskern um einen völlig anderen Button ergänzen. Betrachten Sie eine IDE im Desktop-Stil, deren Benutzeroberfläche absichtlich viel dichter ist als die eines typischen Webseiten. Ihre Buttons verwenden andere Abmessungen, andere Design-Elemente sowie eine andere visuelle Sprache – daher wäre die Verwendung des allgemein einsetzbaren Web-Component-Buttons falsch. Die IDE verfügt einfach über ihren eigenen Angular-Button, und dieser kann weiterhin dieselbe createButton()-Factory aufrufen.

Das ist der praktische Vorteil. Das Ziel ist nicht dieses:

ONE BUTTON
    ↓
use everywhere

Ziel ist Folgendes:

shared behavior
                     │
          ┌──────────┴──────────┐
          ↓                     ↓
   Web Component          Angular component
          ↓                     ↓
      Web UI                 IDE UI

Die Darstellung bleibt auf jedes Produkt beschränkt, während die umständliche Interaktionslogik nicht mehr für jedes Produkt neu geschrieben werden muss.

Web Components machen das rendernde Komponentenkonzept portabel

State Machines sorgen dafür, dass das Verhalten portabel ist. Sie haben keinen Einfluss auf die dargestellte Benutzeroberfläche – und genau darin liegt der Wert von Web Components. Ein Custom Element wie das unten gezeigte gehört zur Web Platform und nicht zu Angular, React oder Vue:

<and-button>
  Save
</and-button>

Angular kann es rendern, Astro kann es ausgeben, React kann es verwenden, und eine einfache HTML-Seite kann es mit einem Script-Tag einbinden. Das umgebende Framework kann sich ändern, während das Element genau gleich bleibt.

In der Praxis variiert die Unterstützung von Frameworks für Custom Elements in den Details. Angular benötigt CUSTOM_ELEMENTS_SCHEMA (oder ein Äquivalent), um unbekannte Tags zu akzeptieren, und React hat historisch Werte als Attribute statt als Eigenschaften übergeben, was die Handhabung von reichhaltigen Daten und benutzerdefinierten Ereignissen erschwerte, obwohl neuere Versionen dies verbessert haben. Es lohnt sich, die aktuelle Unterstützung für Custom Elements Ihres Frameworks zu überprüfen, bevor Sie sich auf diese Architektur einlassen.

Dadurch entstehen zwei verschiedene Arten der Wiederverwendung. Eine ist verhaltensbasierte Portabilität:

State machine
    ↓
portable behavior

Die andere ist eine portable, rendernde Komponente:

Web Component
    ↓
portable rendered component

Manchmal möchte man das gesamte Web Component. Wenn ein Button aus einem Design-System in mehreren Anwendungen identisch aussehen und sich verhalten muss, ist es sinnvoll, ihn als Custom Element zu verpacken. Andere Male möchte man das vollständige Component überhaupt nicht. Das Szenario im IDE ist genau so: Die Interaktionslogik bleibt erhalten, während die Anwendung das Rendering und das Design vollständig selbst steuert.

Es handelt sich dabei nicht um konkurrierende Architekturen; sie ziehen lediglich die Abstraktionsgrenze an unterschiedlichen Stellen. Wenn man in Grenzen statt in Bibliotheken denkt, wird noch etwas klar: Nicht jedes wiederverwendbare UI-Element muss unbedingt ein Component sein.

Layout benötigt kein Component

Das Layout ist das klarste Beispiel. Hier ist ein gewöhnliches Tailwind-Element:

<div
  class="
    flex
    flex-col
    items-start
    gap-6
    p-6
    rounded-xl
    border
    bg-card
    shadow-sm
  "
>
  ...
</div>

Daran ist nichts auszusetzen. Eine der wahren Stärken von Tailwind besteht darin, dass man den Großteil eines Elements verstehen kann, ohne ständig zwischen dem Template und der Stylesheet hin- und herzuspringen. Doch schauen Sie sich an, wie viele Aufgaben dieses einzelne class-Attribut erledigt. Einige Klassen beschreiben die visuelle Identität:

rounded-xl
border
bg-card
shadow-sm

Andere beschreiben die räumlichen Beziehungen zwischen dem Element und seinen Kindern:

flex
flex-col
items-start
gap-6
p-6

Der Browser macht keinen Unterschied zwischen ihnen. class ist einfach ein generisches Mechanismus zur Anhängung von Identifikatoren, die von CSS und JavaScript ausgewählt werden können, und HTML kennt keinen Unterschied zwischen einer „Layout-Klasse“ und einer „Design-Klasse“. Als API-Entscheidung erweist sich die Trennung der beiden jedoch als vorteilhaft. Ein experimentelles and-layout-Attribut drückt den räumlichen Teil eigenständig aus:

<div
  class="card"
  and-layout="vertical align:start gap:lg p:lg"
>
  ...
</div>

Der Vorteil liegt nicht in der Kürze; manchmal ist diese Version sogar nicht kürzer. Der Vorteil besteht darin, dass die Verantwortlichkeiten klar werden: class beschreibt, wie das Element aussieht, und and-layout beschreibt, wie es den Platz anordnet.

Wie das Attribut implementiert wird

Die Implementierung benötigt weder ein Framework noch JavaScript. Es handelt sich um reines CSS: Jedes Token im Attribut wird mit Attributselектoren abgeglichen (der ~=-Selektor für mit Leerzeichen getrennte Wörter eignet sich hier hervorragend) und auf Flexbox, Grid, Abstände sowie responsives Design übertragen. Eine Deklaration wie die folgende ist letztendlich einfach nur CSS Grid mit Breakpoints:

<div
  and-layout="grid cols:1 cols@md:2 cols@lg:3 gap:lg"
>

Hinter diesem Ansatz verbirgt sich kein Layout-Engine, keine Angular-Direktive, keine React-Komponente und auch kein Wrapper – es sei denn, man möchte tatsächlich einen Stack verwenden:

<Stack direction="vertical" gap="lg">

Diese Einschränkung ist wichtig. Layout-Komponenten sind nicht schlecht. Eine <Stack>-Abstraktion kann sehr praktisch sein, insbesondere innerhalb eines Framework-Design-Systems. Der Ansatz mit Attributen ist einfach nur eine weitere Option: Wenn das gewünschte Element bereits existiert, muss man möglicherweise keine zusätzliche Komponente erfinden, nur um zu beschreiben, wie ihre Kinder angeordnet werden.

Ein Kompromiss, den man im Hinterkopf behalten sollte: Benutzerdefinierte Attribute ohne das data--Präfix sind laut Spezifikation kein gültiges HTML, obwohl alle Browser sie gerne stilisieren. Validatoren und einige Linter werden sich beschweren, und eine Schreibweise wie data-and-layout vermeidet das auf Kosten etwas mehr Ausführlichkeit.

class kann alles, muss es aber nicht

Hinter diesem Phänomen steckt ein breiteres Muster. Moderne Markup-Sprachen können einen großen Teil der Verantwortung auf die class-Attribute übertragen. Fügen Sie Utility-CSS sowie ein Animation-Plugin hinzu, und ein ursprünglich völlig normales Element kann zu diesem Zustand werden:

<div
  class="
    card
    flex
    flex-col
    items-center
    gap-6
    p-8
    rounded-xl
    border
    bg-card
    animate-in
    fade-in
    slide-in-from-bottom-4
    duration-500
  "
>

Es funktioniert, und lesbare Utility-Klassen sind weitaus vorzuziehen als die Annahme, jede Schnittstelle benötige eine perfekt semantische BEM-Hierarchie. Die eigentliche Frage ist nicht, ob class all das tragen kann – das ist offensichtlich der Fall. Die Frage lautet vielmehr, ob ein einzelnes Attribut für jede Eigenschaft eines Elements stehen sollte. Durch die Trennung der Aufgaben erhalten Sie:

<div
  class="card"
  and-layout="vertical align:center gap:lg p:xl"
  and-motion="slide-in-up"
  and-motion-duration="500ms"
>

Das Element zeigt nun auf einen Blick drei getrennte Aufgabenbereiche:

class       → visual identity
and-layout  → spatial organization
and-motion  → animation

Betrachten Sie dies als eine kleine Anwendung des Prinzips der einzigen Verantwortung auf Markup-Ebene. HTML erfordert dies nicht. Der Vorteil besteht darin, dass das Ergebnis leichter zu lesen, zu überprüfen und zu ändern ist.

Bewegung erhält ihren eigenen deklarativen Kanal

Die Animationstechnik baut auf einem Muster auf, das seit Jahren gut funktioniert hat. Animate.css, veröffentlicht unter animate.style, steuert Animationen ausschließlich über Klassen: Man fügt die Basisklasse zusammen mit dem Namen der Animation hinzu, und das Element animiert sich.

<h1 class="animate__animated animate__bounce">
  Hello
</h1>

Es ist einfach, vertraut und nahezu ohne Formalitäten. Animation-Plugins, die auf Tailwind ausgerichtet sind, folgen derselben Philosophie, indem sie Animationen in weitere zusammenstellbare Hilfsmittel innerhalb der class umwandeln.

Die interessante Frage hier ist nicht, wie man ein leistungsstärkeres Animationssystem erstellt. Sie spiegelt die Frage zur Layout-Struktur wider: Wenn Animationen eine eigenständige Aufgabe darstellen, können sie im Markup einen eigenen deklarativen Platz haben. Anstatt Animationshilfsmittel neben alles andere zu setzen:

<div
  class="
    card
    animate-in
    fade-in
    slide-in-from-bottom-4
    duration-500
  "
>

beschreibt man die Animation getrennt:

<div
  class="card"
  and-motion="slide-in-up"
  and-motion-duration="500ms"
>

Und wenn der Auslöser wichtig ist, geben Sie ihn zusammen mit Verzögerung und Dauer explizit an:

<div
  class="card"
  and-motion="slide-in-up"
  and-motion-trigger="enter"
  and-motion-duration="800ms"
  and-motion-delay="200ms"
>

Die Markup-Struktur gibt an, welche Animation abläuft, wann sie abläuft und wie sie getaktet wird; die Implementierung kümmert sich um den Rest. Bei einem enter-Auslöser bedeutet das in der Regel, dass ein IntersectionObserver das Element überwacht. Auslöser durch Überfahren mit der Maus oder Tippen verwenden eigene Zuhörer. Entscheidend ist, dass die prefers-reduced-motion-Media-Query an einem zentralen Ort berücksichtigt werden kann, anstatt davon abhängig zu sein, dass jeder Komponentenentwickler daran denkt.

Attribute sind keine Magie. Was sie bieten, ist eine Möglichkeit für ein Element, seine Fähigkeiten anzukündigen, ohne sie in den Komponentenbaum des Frameworks aufzunehmen.

Deklarativ – solange es hilft

Dieser Ansatz hat seine Grenzen. Eine einfache Eingangsanimation passt perfekt in ein einziges Attribut:

and-motion="fade-in"

Die Ausgangsüberleitung eines Modals stellt ein anderes Problem dar. Angenommen, das Modal muss animiert verschwinden und erst nach Abschluss der Animation aus dem DOM entfernt werden. Dann spielen das Lebenszykluskonzept sowie die Reihenfolge eine Rolle, und ein kurzer imperativer Aufruf drückt die Absicht klarer aus:

await player.play('fade-zoom-out');
closeModal();

Der Versuch, jede Beziehung im Lebenszyklus in ein ständig wachsendes HTML-Attribut zu kodieren, würde die deklarative API verschlechtern statt verbessern. Die Leitregel lautet, die einfachste Schicht zu wählen, die das Problem präzise darstellt – sei es reiner CSS, ein Attribut, eine Zustandsmaschine, ein gewöhnliches Framework-Dienst oder ein Web Component. Der deklarative Stil sollte kein Dogma werden. Niemand versucht, JavaScript oder Frameworks loszuwerden. Das Ziel ist es, jedes Problem nicht einfach auf die schwerste verfügbare Abstraktion zu erhöhen, nur weil sie vorhanden ist.

Attribute als kleine Funktions-APIs

Sobald sowohl die Gestaltung als auch die Bewegung diesem Muster folgen, wirken die Attribute weniger wie Konfigurationselemente und eher wie kleine Funktions-APIs. Betrachten Sie diesen Fragment:

<section
  class="feature-card"
  and-layout="vertical gap:lg p:xl"
  and-motion="fade-in"
  and-motion-trigger="enter"
>
  <h2>Framework-agnostic UI</h2>
  <p>At least as much as possible.</p>
</section>

Es handelt sich immer noch um eine <section>, und ihre semantische Bedeutung bleibt unverändert. Um Abstände, die Darstellung sowie eine Einführungsanimation zu erzielen, war kein solcher Stapel aus Wrapper-Elementen erforderlich:

<and-stack>
  <and-motion-container>
    <and-card>
      ...
    </and-card>
  </and-motion-container>
</and-stack>

Diese verschachtelte Version ist nicht automatisch falsch. Wenn diese Elemente eine sinnvolle Funktionalität bieten und eine nützliche API bereitstellen, verdienen sie durchaus den Status von Komponenten. Der Unterschied ist enger: eine wiederverwendbare Fähigkeit muss nicht automatisch zu einer weiteren Komponente werden. Oft existiert bereits ein DOM-Node, der lediglich eine Layout-Anpassung, Bewegungsanimationen oder Tooltip-Funktionen benötigt. Das Framework muss dies nicht immer erkennen, und wenn eine Abstraktion direkt auf HTML basiert, kann sie problemlos in Angular, Astro, React, Vue oder eine Framework-freie Seite übertragen werden.

Framework-unabhängig bedeutet nicht framework-frei

Es besteht hier ein offensichtliches Risiko: „framework-agnostic“ kann leicht zu einem weiteren Reinheitswettbewerb werden, was den eigentlichen Zweck völlig verfehlen würde. Frameworks erlangen ihre Bedeutung durch die Integration von Funktionen. Angular bietet Signale, Templates, Dependency Injection, Formulare, Routing sowie ein klares Anwendungsmodell. React verfügt über ein sehr ausgereiftes Kompositionsökosystem. Vue und Svelte bringen wiederum andere Kompromisse mit sich.

Framework-unabhängiges Verhalten muss dennoch mit dem reaktiven System jeder Anwendung verbunden werden. Genau deshalb stellt Zag Framework-Adapter bereit: Ein Adapter bindet eine Maschine an die Reaktivitäts-, Lebenszyklus- und DOM-Vorgaben eines Frameworks, und die Maschine kann nur unabhängig bleiben, weil eine weitere Schicht das Framework versteht.

Der hier beschriebene schichtweise Ansatz hat die gleichen Nachteile:

  • Ein Angular-Komponent, der auf einer generischen State-Factory basiert, kann einen effect() benötigen, um die Signal-Eingänge mit dem externen Zustand in Einklang zu halten.
  • Eine Web Component muss die State-Schicht mit ihren eigenen Lebenszyklus-Callback-Funktionen verbinden.
  • Das attributgesteuerte Layout führt eine einfache DSL ein, die jedes Teammitglied lernen muss.
  • Motion-Attribute fügen eine weitere API-Oberfläche hinzu.
  • Das Aufteilen aller Komponenten in separate Pakete erzeugt Verträge, die im Laufe der Zeit kompatibel bleiben müssen.

Mannchmal ist ein einfacher, framework-spezifischer Komponentenansatz einfach die bessere Lösung im Design. Deshalb macht ein ausschließlich für Angular konzipiertes Komponenten-Kit weiterhin Sinn neben all dem anderen. Niemand sollte jeden Angular-Button durch eine Kette aus vier Adaptern, einem Paar Zustandsmaschinen und einem Web Component leiten, nur weil „agnostisch“ anspruchsvoll klingt. Das ist eine sehr aufwändige Art, das Schreiben folgender Codeabschnitte zu vermeiden:

<volt-button>

Die Unabhängigkeit vom Framework lohnt sich, wenn Portabilität tatsächlich eine Anforderung ist. Wenn das nicht der Fall ist, ist oft eine enge Integration in das Framework die bessere Lösung.

Framework-agnostische UI als Stack, nicht als Bibliothek

Der Begriff „framework-unabhängige UI-Bibliothek“ ruft in der Regel ein Set an Komponenten hervor, die irgendwie überall funktionieren. Web Components kommen für rendernde Komponenten diesem Ziel ziemlich nahe. Ein nützlicheres Modell ist jedoch nicht eine einzige universelle Bibliothek, sondern ein Aufbau aus verschiedenen Verantwortungsbereichen, wobei jeder Bereich an einem anderen Punkt framework-spezifisch wird:

Application
                         │
          Angular / React / Vue / Astro
                         │
                  framework adapters
                         │
    ─────────────────────────────────────────
                         │
         headless behavior / state machines
                         │
     Web Components     HTML capabilities
          │                 │
          │          layout / motion attributes
          │                 │
    ─────────────────────────────────────────
                         │
                    Web Platform

Keine Anwendung muss alle Schichten nutzen:

  • Ein Projekt verwendet eine Web Component direkt und berührt niemals den darunterliegenden headless-Status.
  • Ein anderes Projekt nutzt nur die Zustandsmaschine und baut daraufhin seine eigene Angular-UI.
  • Eine statische Astro-Seite benötigt möglicherweise nichts weiter als die Layout- und Motion-Attribute.
  • Eine Angular-Anwendung kann den gesamten Aufbau ignorieren und stattdessen ein auf Angular zugeschnittenes Kit mit Angular-Primitiven verwenden, da dies die beste Entwicklererfahrung bietet.

Die Idee besteht darin, frei kombinieren zu können. Framework-unabhängig bedeutet nicht, dass das Framework verboten ist. Es bedeutet vielmehr dass Sie selbst entscheiden, wo die Grenze des Frameworks liegt, anstatt zuzulassen, dass sie automatisch in jede wiederverwendbare Schicht übergeht.

Das Framework auf der Anwendungs Schicht halten

Das ist kein Argument gegen Angular oder ein anderes Framework. Es geht vielmehr darum, welche Verantwortung ein Framework standardmäßig übernimmt. Betrachten Sie die Beispiele erneut aus dieser Perspektive:

  • Das visuelle Design eines Dropdown-Menüs kann zum Produkt gehören und seine Darstellung von Angular übernommen werden, doch sein Interaktionsmodell muss das nicht.
  • Ein gemeinsamer Button kann eine Web Component sein, wenn Sie denselben Button in mehreren Projekten verwenden möchten, oder eine benutzerdefinierte Angular-Komponente, die nur einen kleinen Verhaltenskern teilt, wenn ein Produkt seine eigene visuelle Sprache benötigt.
  • Eine Karte kann mit Tailwind gestylt werden und dennoch ihr Layout in and-layout beibehalten.
  • Ein Eingangs-Effekt benötigt möglicherweise nichts weiter als einen kleinen Observer und ein Attribut, und es sollte egal sein, ob Astro oder Angular die HTML-Datei erzeugt hat.
  • Zusammen gesehen passen alle Elemente zusammen:

    • Headless UI entfernt die visuellen Aspekte.
    • State Machines befreien das Verhalten von bestimmten Renderern.
    • Web Components ermöglichen es fertigen, renderierten Komponenten, zwischen verschiedenen Frameworks zu verwendet werden.
    • Eigens entwickelte Attribute verknüpfen leichte Funktionen wie Layout und Bewegung direkt mit dem HTML selbst statt mit dem Framework.

    Niemand dieser Ideen ist neu, und nicht jedes UI-System sollte auf diese Weise entwickelt werden. Zusammen stellen sie jedoch eine seit langem geltende Gewohnheit in Frage: nämlich dass das Framework auch unter jeder wiederverwendbaren UI-Abstraktion einer Anwendung liegen muss.

    Kernpunkte

    • Unterscheiden Sie zwischen „unstyled“ und „renderer-unabhängig“; die meisten headless-Bibliotheken gehören nur zur ersten Kategorie.
    • Platzieren Sie die Interaktionslogik, die mehrere Produkte teilen, in frameworkfreien Zustandsmaschinen oder Factories und akzeptieren Sie bewusst die damit verbundenen Kosten.
    • Verwenden Sie Web Components, wenn das rendernde Komponenten selbst – nicht nur sein Verhalten – in allen Umgebungen identisch sein muss.
    • Wenden Sie CSS-einziges Attributverhalten für Layout und einfache Bewegungen an und wechseln Sie zu imperativen Codestrukturen, sobald die Reihenfolge im Lebenszyklus eine Rolle spielt.
  • Halten Sie framework-spezifische Komponenten dort, wo Portabilität keine Anforderung ist; das Ziel besteht darin, Frameworks zu verwenden, ohne dass jede Schicht von ihnen abhängig wird.
  • Verwandte Artikel