@@Felix Riesterer
… möchte kritisiert und verbessert werden:
Nachdem die Diskussion etwas ausgeschweift ist, zurück zum Artikel. Was wäre noch zu verbessern?
Nun, das Markup. Es ist so gut wie nicht vorhanden. Nur leere Tabellenzellen. Aber was sollten die auch für Inhalt haben?
Dazu fragen wir uns, was denn die Gundfunktionalität ist: Ganz einfach die Auswahl von Kreuz oder Kreis in jedem Feld.
1. Tic
Jede Zelle hat initial keinen Wert und kann durch Nutzerinteraktion mit ❌ (×, x) oder ⭕ (○, o) befüllt werden. Für 0 und 1 hatte ich schon eine Möglichkeit angedeutet.
Aber das passende UI-Element zur Auswahl ist ein Drop-Down-Menü. Das Innere der Tabellenzellen wäre also:
<td>
<label for="top-left">top left field</label>
<select id="top-left">
<option></option>
<option value="x" aria-label="x">❌</option>
<option value="o" aria-label="o">⭕</option>
</select>
</td>
Und so sieht’s aus. Nicht besonders schön, aber völlig funktional. So funktional, wie es mit HTML eben sein kann.
Das kann sogar ein Blinder bedienen. (Und das ist wörtlich gemeint. ♿️ Deswegen auch das aria-label
-Attribut.)
2. Tac
Das Aussehen können wir ja verbessern. Aussehen heißt CSS.
legend
wird visuell versteckt; die select
-Box wird größer und ihren Rahmen los. (War das jetzt schon ein Zeugma?) Das war’s dann auch schon im Wesentlichen mit dem Styling.
Sieht schon besser aus, aber die Funktionalität ist noch unterste Stufe. Jeder Spieler ist selbst dafür verantwortlich, dass er sein Symbol auswählt, und beide dafür, dass sie abwechselnd ziehen.
3. Toe
Hier (erst!!) kommt nun JavaScript ins Spiel.
if (document.querySelector)
Außer in alten Browsern. Aber das ist völlig OK – progressive enhancement. Die Grundfunktionalität ist ja auch in alten Browsern gegeben.
{
document.documentElement.classList.add('js');
Wenn JavaScript ausgeführt wird, bekommt das html
-Element eine Klasse js
.
In einer booleschen Variablen isPlayerXMoving
wird festgehalten, wer am Zug ist. Die Tabelle bekommt einen Eventhandler verpasst. (Und nicht etwa jedes select
einen eigenen – wir nutzen event delegation.) Darin passiert folgendes:
function ticTacToeClickHandler(event)
{
var targetElement = event.target;
if (targetElement.nodeName == 'SELECT')
… nur für select
-Elemente; nicht da, wo das Event sonst noch so vorbeibubblet.
{
targetElement.blur();
Der Fokus wird schnell wieder vom select
-Element weggenommen, damit das Aufklappen nicht (oder kaum) zu sehen ist. Aus demselben Grund werden per Stylesheet auch die option
s ausgeblendet, mehr dazu später.
if (!targetElement.disabled)
{
targetElement.value = isPlayerXMoving ? 'x' : 'o';
targetElement.disabled = true;
Das Feld erhält, wenn es noch frei ist, den entsprechenden Wert – je nachdem, wer gerade am Zug war. Dann wird es disablet, damit es nicht noch einmal ausgewählt werden kann.
isPlayerXMoving = !isPlayerXMoving;
}
}
}
Der nächste Spieler ist am Zug.
Um die nun nicht benötigten option
-Elemente auszublenden, erhält das Stylesheet noch eine Ergänzung:
.js #tic-tac-toe option
{
display: none;
}
Diese Regel wirkt nur dann, wenn ein Vorfahrenelement die Klasse js
hat. Das ist nur dann der Fall, wenn JavaScript ausgeführt wird, da wir diese Klasse fürs html
-Element ja erst mit JavaScript gesetzt hatten.
Und so haben wir das Grundkonstrukt progressively enhanced – erst das Aussehen, dann das Verhalten; mit schon ansehnlichem Ergebnis.
Bei Ausfall von JavaScript oder CSS funktioniert die Grundfunktionalität immer noch – und zwar auch, wenn CSS ausfällt, JavaScript aber ausgeführt wird. Dann sieht das Feld wieder ungestylt aus, das JavaScript sorgt aber schon dafür, dass in den disableten select
s nicht erneut auswählt werden kann.
Jetzt fehlt noch die Abfrage, ob ein Spieler „was auf die Reihe gekriegt hat“ – aber die sei mal out of scope dieses Postings.
select
s sind recht störrisch gegenüber Styling. WebKits können nicht die Schriftgröße der option
s kleiner setzen als die des select
s. Auch die Positionierung des Drop-Downs innerhalb des Feldes ist Frickelei, wenn jeder Browser da etwas andere Vorstellungen hat.
Beschäftigen wir uns lieber mit etwas anderem:
Eine Auswahl von einer Option aus mehreren kann auch durch eine Gruppe von Radiobuttons geschehen.
1. Tic
Entgegen früheren HTML-Versionen, wo ohne Nutzerinteraktion der erste Radiobutton einer Gruppe als vorausgewählt galt, wenn keiner als selected
gekennzeichnet war (woran sich Browser aber nicht gehalten haben), sieht HTML5 explizit vor, dass initial kein Radiobutton ausgewählt sein muss.
Das Innere der Tabellenzellen wäre hier derart:
<td>
<fieldset>
<legend>top left field</legend>
<input type="radio" name="top-left" id="top-left-x">
<label for="top-left-x">x</label>
<input type="radio" name="top-left" id="top-left-o">
<label for="top-left-o">o</label>
</fieldset>
</td>
Wieder nicht besonders schön, aber funktioniert. Und ist blind bedienbar. ♿️
2. Tac
Und wieder setzen wir progressive enhancement ein, um Darstellung und Verhalten schrittweise zu verbessern. Zuerst wieder das Styling:
Die Radiobuttons werden ausgeblendet, fieldset
büßt seinen Rahmen ein und auch hier wird legend
wieder visuell versteckt – und der Labeltext auch; dafür kommt SVG zum Einsatz. Und nicht outline
für :focus
vergessen!
Die Magie liegt nun darin:
#tic-tac-toe :checked + label::after
{
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
Wenn ein Radiobutton ausgewählt wird, wird das zugehörige Label vergrößert, so dass es das ganze Feld ausfüllt.
Diese Vergrößerung könnte man auch animieren; die betreffende Codezeile ist aber auskommentiert, denn diese Animation ist nicht gerade performant. Besser ist es, transition
bspw. auf transform
anzuwenden.
@supports (transform: scale(1))
{
Dazu fragen wir ab, der der Browser transform
denn kann. (Andernfalls würden wir die Vergrößerung in alten Browsern nicht mehr haben.)
#tic-tac-toe label::after
{
width: 100%;
height: 100%;
transform: scale(0.25);
transition: transform 0.1s;
}
Initial werden die Label verkleinert, damit sie nebeneinander passen.
#tic-tac-toe label[for$="-x"]::after
{
transform-origin: left top;
}
#tic-tac-toe label[for$="-o"]::after
{
left: 0;
transform-origin: center top;
}
Nebeneinander, nicht aufeinander! Deshalb haben sie verschiedene Streckungszentren.
#tic-tac-toe :checked + label::after
{
transform: scale(1);
}
}
Und bei ausgewähltem Radiobutton füllt das zugehörige Label das ganze Feld.
Doch bei dieser Darstellung kommt wohl kaum noch jemand drauf, dass man hier Radiobuttons auswählt.
3. Toe
Das Label für den nicht ausgewähltem Radiobutton soll natürlich noch verschwinden und auch die Wer-ist-am-Zug?-Logik implementiert werden.
Auch hier wieder ein Eventhandler und event delegation.
function ticTacToeClickHandler(event)
{
var targetElement = event.target;
if (targetElement.nodeName == 'INPUT')
{
targetElement.parentNode.disabled = true;
switchPlayer();
}
}
Ähnlich wie oben, nur dass die Gruppe der Radiobuttons mittels deren Elternelement (das wäre hier fieldset
) disablet wird. Und dass hier der Code, der initial auch einmal ausgeführt werden muss, in die Funktion switchPlayer
ausgelagert ist.
function switchPlayer()
{
isPlayerXMoving = !isPlayerXMoving;
Der nächste ist dran.
for (var i = 0; i < xInputElements.length; i++)
{
xInputElements[i].disabled = !isPlayerXMoving;
}
for (var i = 0; i < oInputElements.length; i++)
{
oInputElements[i].disabled = isPlayerXMoving;
}
}
Wenn der Spieler am Zug ist, der die Kreuze macht, werden die Kreuz-Radiobuttons enablet; die Kreis-Radiobuttons disablet. Für den anderen Spieler entsprechend andersrum.
Im Stylesheet sind noch einige Anpassungen nötig. Damit diese nur greifen, wenn JavaScript ausgeführt wird, wieder mit .js
im Selektor.
.js #tic-tac-toe label::after
{
left: 0;
width: 100%;
height: 100%;
transform: scale(1);
opacity: 0;
transition: none;
z-index: 1;
}
Die Label lassen wir das gesamte Feld ausfüllen, damit die Spieler überall im Feld clicken können.
.js #tic-tac-toe :disabled + label::after,
.js #tic-tac-toe :disabled > label::after
{
z-index: 0;
opacity: 0;
cursor: not-allowed;
}
Label von disableten Radiobuttons werden nicht angezeigt. Dass dann das Feld nicht anclickbar ist, wird durch einen entsprechenden Cursor angezeigt.
.js #tic-tac-toe :checked + label::after
{
opacity: 1;
}
Label von ausgewählten Radiobuttons werden angezeigt.
Das Ganze sieht damit so aus – Radiobuttons progressively enhanced.
Auch diese Lösung funktioniert, wenn JavaScript interpretiert wird, CSS aber nicht. disablete Radiobuttons können nicht ausgewählt werden und werden ausgegraut dargestellt.
An der Stelle käme dann wieder die Erkennung des Spielendes hinzu, um die wir uns in diesem Posting nicht kümmern wollen.
--
Kümmern wir uns lieber um die grundsätzliche Frage: Sollte das alles in einem Tutorial für Anfänger stehen?
Ja, unbedingt!! Wie sonst sollen Anfänger das Prinzip von progressive enhancement verinnerlichen, wenn es ihnen in Tutorials nicht so vorgemacht wird? Kein Anfänger wird nach einem solchen JavaScript-Tutorial noch ein zweites lesen, denn die Lösung „funzt“ ja. Nur dass sie eben nicht funktioniert.
Wollen wir die nächste Generation von Entwicklern heranzüchten, die das dreiundzwölfzigste JavaScript-Framework entwickeln, ohne die geringste Ahnung von HTML zu haben? Was man dem erzeugten Code auch ansieht und das Ergebnis zu Lasten von UX und Barrierefreiheit, also zu Lasten der Nutzer geht?
LLAP 🖖
--
„Wir haben deinen numidischen Schreiber aufgegriffen, o Syndicus.“
„Hat auf dem Forum herumgelungert …“
(Wachen in Asterix 36: Der Papyrus des Cäsar)