Zustandsmaschinen werden typischerweise im Web in JavaScript und oft über die beliebte XState Bibliothek ausgedrückt. Aber das Konzept einer Zustandsmaschine ist an fast jede Sprache anpassbar, einschließlich, erstaunlicherweise, HTML und CSS. In diesem Artikel werden wir genau das tun. Ich habe kürzlich eine Website erstellt, die die Einschränkung "kein Client-JavaScript" enthielt und ich benötigte eine besondere, einzigartige interaktive Funktion.
Der Schlüssel zu all dem ist die Verwendung von <form> und <input type="radio"> Elementen zur Speicherung eines Zustands. Dieser Zustand wird mit einem anderen Radio <input> oder einem zurücksetzbaren <button> umgeschaltet oder zurückgesetzt, der sich überall auf der Seite befinden kann, da er mit demselben <form> Tag verbunden ist. Ich nenne diese Kombination einen Radio-Reset-Controller, und er wird am Ende des Artikels detaillierter erklärt. Mit zusätzlichen Formular-/Input-Paaren können Sie komplexere Zustände hinzufügen.
Es ist ein wenig wie der Checkbox-Hack, da letztendlich der :checked Selektor in CSS die UI-Arbeit verrichtet, aber dies ist logisch fortgeschrittener. Ich verwende am Ende eine Template-Sprache (Nunjucks) in diesem Artikel, um es überschaubar und konfigurierbar zu halten.
Ampel-Zustandsmaschine
Jede Erklärung einer Zustandsmaschine muss das obligatorische Ampel-Beispiel enthalten. Unten ist eine funktionierende Ampel, die eine Zustandsmaschine in HTML und CSS verwendet. Das Klicken auf "Next" (Weiter) schaltet den Zustand weiter. Der Code in diesem Pen wurde aus der Zustandsmaschinen-Vorlage verarbeitet, um in einen Pen zu passen. Wir werden uns den Code später in einer lesbareren Form ansehen.
Informationen in Tabellen ausblenden/anzeigen
Ampeln sind keine besonders praktischen alltäglichen UI-Elemente. Wie wäre es stattdessen mit einer <table>?
Es gibt zwei Zustände (A und B), die von zwei verschiedenen Stellen im Design verändert werden und sich auf Änderungen in der gesamten Benutzeroberfläche auswirken. Dies ist möglich, da die leeren <form> Elemente und <input> Elemente, die den Zustand speichern, ganz oben im Markup stehen und somit ihr Zustand durch allgemeine Geschwister-Selektoren ermittelt werden kann und der Rest der Benutzeroberfläche über Nachfahren-Selektoren erreicht werden kann. Hier gibt es eine lose Kopplung von UI und Markup, was bedeutet, dass wir den Zustand fast von allem auf der Seite *von* überall auf der Seite aus ändern können.
Allgemeine Vier-Zustands-Komponente

Das Ziel ist eine universell einsetzbare Komponente zur Steuerung des gewünschten Zustands der Seite. "Seitenstatus" bezieht sich hier auf den gewünschten Zustand der Seite und "Maschinenstatus" bezieht sich auf den internen Zustand des Controllers selbst. Das obige Diagramm zeigt diese generische Zustandsmaschine mit vier Zuständen (A, B, C und D). Die vollständige Zustandsmaschine des Controllers hierfür ist unten dargestellt. Sie wird mit drei der Radio-Reset-Controller-Bits aufgebaut. Drei davon zusammen bilden eine Zustandsmaschine, die acht interne Maschinenzustände hat (drei unabhängige Radio-Buttons, die entweder ein- oder ausgeschaltet sind).

Die "Maschinenzustände" werden als Kombination der drei Radio-Buttons geschrieben (d.h. M001 oder M101). Um vom Anfangszustand M111 zu M011 zu wechseln, wird der Radio-Button für dieses Bit durch Klicken auf einen anderen Radio <input> in derselben Gruppe abgewählt. Um zurückzukehren, wird der Reset <button> für das <form>, das mit diesem Bit verbunden ist, geklickt, was den Standard-Checked-Zustand wiederherstellt. Obwohl diese Maschine insgesamt acht Zustände hat, sind nur bestimmte Übergänge möglich. Zum Beispiel gibt es keine Möglichkeit, direkt von M111 zu M100 zu gelangen, da dies erfordert, dass zwei Bits umgeschaltet werden. Aber wenn wir diese acht Zustände zu vier Zuständen falten, so dass jeder Seitenstatus zwei Maschinenzustände teilt (z.B. A teilt die Zustände M111 und M000), dann gibt es einen einzelnen Übergang von jedem Seitenstatus zu jedem anderen Seitenstatus.
Wiederverwendbare Vier-Zustands-Komponente
Für die Wiederverwendbarkeit wird die Komponente mit Nunjucks-Template-Makros erstellt. Dies ermöglicht es, sie auf jeder Seite einzufügen, um eine Zustandsmaschine mit den gewünschten gültigen Zuständen und Übergängen hinzuzufügen. Es gibt vier erforderliche Unterkomponenten
- Controller
- CSS-Logik
- Übergangssteuerungen
- Zustandsklassen
Controller
Der Controller wird mit drei leeren Formular-Tags und drei Radio-Buttons erstellt. Jedes der checked Attribute der Radio-Buttons ist standardmäßig checked. Jeder Button ist mit einem der Formulare verbunden und sie sind voneinander unabhängig mit eigenen Radio-Gruppennamen. Diese Inputs sind mit display: none versteckt, da sie nicht direkt verändert oder gesehen werden. Die Zustände dieser drei Inputs bilden den Maschinenstatus, und dieser Controller wird am oberen Rand der Seite platziert.
{% macro FSM4S_controller()%}
<form id="rrc-form-Bx00"></form>
<form id="rrc-form-B0x0"></form>
<form id="rrc-form-B00x"></form>
<input data-rrc="Bx00" form="rrc-form-Bx00" style="display:none" type="radio" name="rrc-Bx00" checked="checked" />
<input data-rrc="B0x0" form="rrc-form-B0x0" style="display:none" type="radio" name="rrc-B0x0" checked="checked" />
<input data-rrc="B00x" form="rrc-form-B00x" style="display:none" type="radio" name="rrc-B00x" checked="checked" />
{% endmacro %}
CSS-Logik
Die Logik, die den obigen Controller mit dem Zustand der Seite verbindet, ist in CSS geschrieben. Der Checkbox-Hack verwendet eine ähnliche Technik, um Geschwister- oder Nachfahrenelemente mit einer Checkbox zu steuern. Der Unterschied hier ist, dass der den Zustand steuernde Button nicht eng mit dem ausgewählten Element gekoppelt ist. Die folgende Logik wählt basierend auf dem "checked"-Zustand jedes der drei Controller-Radio-Buttons und jedem Nachfahrenelement mit der Klasse .M000 aus. Diese Zustandsmaschine verbirgt jedes Element mit der Klasse .M000, indem sie display: none !important setzt. Das !important ist kein wesentlicher Teil der Logik hier und könnte entfernt werden; es priorisiert lediglich das Verstecken vor dem Überschreiben durch andere CSS.
{%macro FSM4S_css()%}
<style>
/* Hide M000 (A1) */
input[data-rrc="Bx00"]:not(:checked)~input[data-rrc="B0x0"]:not(:checked)~input[data-rrc="B00x"]:not(:checked)~* .M000 {
display: none !important;
}
/* one section for each of 8 Machine States */
</style>
{%endmacro%}
Übergangssteuerung
Die Änderung des Zustands der Seite erfordert einen Klick oder eine Tastenbetätigung vom Benutzer. Um ein einzelnes Bit des Maschinenstatus zu ändern, klickt der Benutzer auf einen Radio-Button, der mit demselben Formular und derselben Radio-Gruppe eines der Bits im Controller verbunden ist. Um ihn zurückzusetzen, klickt der Benutzer auf einen Reset-Button für das Formular, das mit demselben Radio-Button verbunden ist. Der Radio-Button oder der Reset-Button wird nur angezeigt, je nachdem, in welchem Zustand sie sich befinden. Ein Übergangs-Makro für jeden gültigen Übergang wird zum HTML hinzugefügt. Es können mehrere Übergänge überall auf der Seite platziert werden. Alle Übergänge für derzeit inaktive Zustände werden ausgeblendet.
{%macro AtoB(text="B",class="", classBtn="",classLbl="",classInp="")%}
<label class=" {{class}} {{classLbl}} {{showM111_A()}} "><input class=" {{classInp}} " form="rrc-form-Bx00" type="radio" name="rrc-Bx00" />{{text}}</label>
<button class=" {{class}} {{classBtn}} {{showM000_A1()}} " type="reset" form="rrc-form-Bx00">{{text}}</button>
{%endmacro%}
Zustandsklasse
Die drei oben genannten Komponenten sind ausreichend. Jedes Element, das vom Zustand abhängt, sollte die angewendeten Klassen haben, um es während anderer Zustände zu verstecken. Das wird unübersichtlich. Die folgenden Makros werden verwendet, um diesen Prozess zu vereinfachen. Wenn ein bestimmtes Element nur im Zustand A angezeigt werden soll, fügt das {{showA()}} Makro die Zustände hinzu, um es zu verstecken.
{%macro showA() %}
M001 M010 M100 M101 M110 M011
{%endmacro%}
Alles zusammenfügen
Das Markup für das Ampel-Beispiel ist unten dargestellt. Die Template-Makros werden in der ersten Zeile der Datei importiert. Die CSS-Logik wird in den Kopfbereich und der Controller an den Anfang des Body eingefügt. Die Zustandsklassen befinden sich auf jeder der Ampeln des .traffic-light Elements. Das leuchtende Signal hat ein {{showA()}} Makro, während die "ausgeschaltete" Version des Signals die Maschinenzustände für die Klassen .M000 und .M111 hat, um es im Zustand A zu verbergen. Der Übergangsknopf befindet sich am Ende der Seite.
{% import "rrc.njk" as rrc %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Traffic Light State Machine Example</title>
<link rel="stylesheet" href="styles/index.processed.css">
{{rrc.FSM4S_css()}}
</head>
<body>
{{rrc.FSM4S_controller()}}
<div>
<div class="traffic-light">
<div class="{{rrc.showA()}} light red-light on"></div>
<div class="M111 M000 light red-light off"></div>
<div class="{{rrc.showB()}} light yellow-light on"></div>
<div class="M100 M011 light yellow-light off"></div>
<div class="{{rrc.showC()}} light green-light on"></div>
<div class="M010 M101 light green-light off"></div>
</div>
<div>
<div class="next-state">
{{rrc.AtoC(text="NEXT", classInp="control-input",
classLbl="control-label",classBtn="control-button")}}
{{rrc.CtoB(text="NEXT", classInp="control-input",
classLbl="control-label",classBtn="control-button")}}
{{rrc.BtoA(text="NEXT", classInp="control-input",
classLbl="control-label",classBtn="control-button")}}
</div>
</div>
</div>
</body>
</html>
Erweiterung auf mehr Zustände
Die hier vorgestellte Zustandsmaschinen-Komponente enthält bis zu vier Zustände, was für viele Anwendungsfälle ausreicht, insbesondere da es möglich ist, mehrere unabhängige Zustandsmaschinen auf einer Seite zu verwenden.
Das gesagt, kann diese Technik verwendet werden, um eine Zustandsmaschine mit mehr als vier Zuständen zu erstellen. Die folgende Tabelle zeigt, wie viele Seitenstatus durch Hinzufügen zusätzlicher Bits erstellt werden können. Beachten Sie, dass eine gerade Anzahl von Bits nicht effizient zusammenfällt, weshalb drei und vier Bits beide auf vier Seitenstatus beschränkt sind.
| Bits (rrcs) | Maschinen-Zustände | Seiten-Zustände |
|---|---|---|
| 1 | 2 | 2 |
| 2 | 4 | 2 |
| 3 | 8 | 4 |
| 4 | 16 | 4 |
| 5 | 32 | 6 |
Details zum Radio-Reset-Controller
Der Trick, um ein HTML-Element überall auf der Seite ohne JavaScript anzeigen, ausblenden oder steuern zu können, ist das, was ich einen Radio-Reset-Controller nenne. Mit drei Tags und einer Zeile CSS können der steuernde Button und das gesteuerte Element überall *nach* diesem Controller platziert werden. Die gesteuerte Seite verwendet einen versteckten Radio-Button, der standardmäßig checked ist. Dieser Radio-Button ist durch eine ID mit einem leeren <form> Element verbunden. Dieses Formular hat einen type="reset" Button und einen weiteren Radio-Input, die zusammen den Controller bilden.
<!-- RRC Controller -->
<form id="rrc-form"></form>
<label>
Show
<input form="rrc-form" type="radio" name="rrc-group" />
</label>
<button type="reset" form="rrc-form">Hide</button>
<!-- Controlled by RRC -->
<input form="rrc-form" class="hidden" type="radio" name="rrc-group" checked />
<div class="controlled-rrc">Controlled from anywhere</div>
Dies zeigt eine minimale Implementierung. Der versteckte Radio-Button und die div, die er steuert, müssen Geschwister sein, aber dieser Input ist versteckt und muss niemals direkt vom Benutzer interaktiv genutzt werden. Er wird durch einen standardmäßigen checked Wert gesetzt, durch den anderen Radio-Button gelöscht und durch den Formular-Reset-Button zurückgesetzt.
input[name='rrc-group']:checked + .controlled-rrc {
display: none;
}
.hidden {
display: none;
}
Nur zwei Zeilen CSS sind erforderlich, um dies zu erreichen. Der Pseudo-Selektor :checked verbindet den versteckten Input mit dem Geschwister, das er steuert. Er fügt den Radio-Button und den Reset-Button hinzu, die als einzelner Umschalter gestaltet werden können, was im folgenden Pen gezeigt wird.
Barrierefreiheit… sollten Sie das tun?
Dieses Muster funktioniert, aber ich schlage nicht vor, dass es überall und für alles verwendet werden sollte. In den meisten Fällen ist JavaScript der richtige Weg, um Interaktivität ins Web zu bringen. Mir ist bewusst, dass das Posten dieser Information möglicherweise Kritik von Barrierefreiheits- und semantischen Markup-Experten hervorruft. Ich bin kein Barrierefreiheitsexperte, und die Implementierung dieses Musters kann Probleme verursachen. Oder auch nicht. Ein richtig beschrifteter Button, der etwas mit der Seite macht, gesteuert durch ansonsten versteckte Inputs, könnte gut funktionieren. Wie alles andere im Bereich Barrierefreiheit: Tests sind erforderlich.
Außerdem habe ich noch niemanden gesehen, der darüber schreibt, wie man das macht, und ich denke, das Wissen ist nützlich – auch wenn es nur in seltenen oder Randfällen angemessen ist.
Das ist ziemlich erstaunlich. Ich hatte vergessen, dass man Inputs außerhalb eines Formulars haben kann.
In Großbritannien gehen Ampeln von Rot, Rot+Gelb, Grün, Gelb, Rot. Wir haben also 4 Zustände, die sich zyklisch abwechseln.
Ich glaube nicht, dass ich CSS verwendet habe, um Zustandsmaschinen zu erstellen, obwohl grundlegende Radio-Controls/Checkboxes entweder an- oder aus oder undefiniert/unbestimmt sind.
Ich frage mich, ob der angeforderte CSS-Parent-Selektor die Markup-Struktur verbessern könnte, damit ich ein Steuerelement habe, das jedes Element im HTML steuert.
Schauen Sie sich die Nachbildung eines Buttons an, der in der Amiga Workbench UI verwendet wurde: https://kawalekkodu.pl/examples/cycle-button2.html
Wow, das ist komplexer mit CSS