Fragen zu HTML5 und Co beantwortet 23 - Semikolons, Flexbox-Breiten, Formulare

Wieder haben mich viele neue Fragen zu HTML5, CSS3 und JavaScript über die diversen Kanäle erreicht. Dass es nicht zu mehr Blogposts kommt, ist allein meine Schuld … aber genug gejammert, ran an die neusten Leserfragen!

Unterschied width/flex-basis

Was ist bei einem Flexbox-Layout der Unterschied zwischen width und flex-basis? Gibt es überhaupt einen?

Beim Bau eines 0815-Spaltenlayouts ist es so gut wie egal, ob man width oder flex-basis verwendet – bei der einfachen Anordnung von Boxen nebeneinander führen beide Eigenschaften zum gleichen Ergebnis. Allerdings gibt es durchaus Unterschiede, die je nach Use Case durchaus ins Gewicht fallen können:

  • Vielleicht offensichtlich, aber dennoch erwähnenswert: flex-basis funktioniert nur im Flexbox-Kontext, width greift hingegen immer. Wenn man eine wiederverwertbare Komponente gestaltet, die mal mit und mal ohne Flexbox verwendet werden soll, ist das ein Argument für width.
  • Der Effekt von flex-basis ist von der gewählten flex-direction abhängig. Während width immer die horizontale Ausdehnung eines Elements steuert, kann flex-basis, wenn flex-direction auf column oder column-reverse steht, für die Höhe zuständig sein.
  • Während width auch bei absolut positionierten Elementen funktioniert, ist das bei Flex-Items nicht der Fall
  • Die praktische Abkürz-Eigenschaft flex fasst flex-basis mit flex-shrink und flex-growzusammen. Für width gibt es nichts Entsprechendes.

Hier eine kleine Demo der Gemeinsamkeiten und Unterschiede.

Semikolons nach Funktionsdeklarationen

Warum muss ich in JavaScript bei Funktionsdeklarationen kein Semikolon verwenden? Ich weiß Semikolons sind optional, aber bei Funktionsdeklarationen sehe ich so gut wie nie Semikolons. Warum ist das so?

Die ECMAScript-Spezifikationen verlangen tatsächlich nach Funktionsdeklarationen kein Semikolon (wenn eins da ist, stört es aber nicht). Zur Einordnung: das hier ist eine Funktionsdeklaration …

function foo(){
  return 42;
}

… und im Vergleich dazu ein Funktionsausdruck, nach dem man (wenn man mal die automatische Semikolon-Einfügung ignoriert) ein Semikolon haben sollte:

var foo = function(){
  return 42;
};

Und warum ist das so? Semikolons trennen in JavaScript Statements voneinander. Funktionsdeklarationen sind aber keine Statements, sondern fallen in die Kategorie Declaration. Der beobachtbare Hauptunterschied ist, dass Funktionsdeklarationen evaluiert werden, bevor das Script tatsächlich ausgeführt wird. Das kann man daran erkennen, dass Funktionsdeklarationen aufgerufen werden können, bevor sie im Code definiert werden, was mit einem Funktionsausdruck nicht klappt:

// Function Declaration
foo(); // Klappt
function foo(){
  window.alert(23);
}

// Function Expression
bar(); // Klappt nicht
var bar = function(){
  window.alert(42);
};

Lange Rede, kurzer Sinn: der Job, den Semikolons in JavaScript machen, wird von Funktionsdeklarationen nicht benötigt.

Formulardaten lokal speichern

Ich möchte in ein Formular eingegebene Daten lokal speichern. Muss ich dafür ein jQuery-Plugin benutzen oder geht das auch mit HTML5-Bordmitteln?

Mit FormData gibt es eine recht einfache API um die eigegebenen Daten aus einem Formular zu extrahieren. Das von new FormData(someFormElement) zurückgegebene Element hat eine entries() Methode, die einen Iterator über die Name-Wert-Paare der Formulardaten zurückgibt – und von da aus ist der Weg zum speicherbaren JSON nicht mehr weit:

document.querySelector("input[value=Speichern]").addEventListener("click", () => {
  // Iterator mit [name, value]
  const data = new FormData(document.querySelector("form")).entries();
  // Daten als Objekte in einem Array
  const serialized = Array.from(data, ([name, value]) => ({ name, value }));
  // JSON, bereit zum Speichern in DOM Storage!
  const json = JSON.stringify(serialized);
  console.log(json);
});

So einfach kann es sein! Wichtig ist: FormData-Objekte können ohne Serialisierung direkt von XMLHttpRequest.send() verschickt werden und die IndexedDB kann zumindest die aus dem Iterator erzeugten Arrays speichern, ganz ohne JSON.

Warum funktioniert mein Custom Error nach dem Submit-Event nicht?

Ich habe ein Formular gebaut, das beim Submit-Event ein paar Dinge validiert und im Fehlerfall mit setCustomValidity() HTML5-Validierungsfehler auf den Feldern auslösen soll. Das scheint aber in keinen Browser zu funktionieren. Was ist da los?

Wenn man mit setCustomValidity() Felder als ungültig markieren möchte, muss man das vor dem Submit-Event erledigen. Der Sinn von setCustomValidity() ist das Festlegen des Fehler-Zustandes eines Feldes, nicht an Anzeigen der Fehlermeldung– das erledigt der Browser beim bzw. kurz vor dem Abschicken des Formulars selbst. Ein kleiner Auszuug aus dem Form submission algorithm von HTML5:

When a form element form is submitted from an element submitter (typically a button), optionally with a submitted from submit() method flag set, the user agent must run the following steps:

  1. If [...] the constraint validation concluded that there were invalid fields and probably informed the user of this [...] fire a simple event named invalid at the form element and then abort these steps.
  2. [...] then fire a simple event [...] named submit, at form.

Man sieht: die Validierung findet vor dem Submit-Event statt. Deshalb sollte sollte setCustomValidity() dann passieren, wenn sich der Inhalt eines Feldes ändert (z.B. bei change- oder keyup-Events).

Weitere Fragen?

All diese Fragen wurden mir per E-Mail oder Twitter gestellt und auch eure Fragen zu HTML(5), CSS(3), JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach über einen der genannten Kanäle anschreiben oder gleich das komplette Erklärbären-Paket kommen lassen.

Fragen zu HTML5 und Co beantwortet 22 - Addition, Polyfills, Service Worker, Breakpoints für Responsive

Hallo! Ja, ich lebe noch. Ich hatte bloß ein paar Monate lang viel zu viel Arbeit am Hals und habe Webtech-Fragen nur per E-Mail oder in meinen Veranstaltungen bearbeiten können. Aber allein schon um die ganzen Spammer- und Webseitenaufkauf-Anfragen endlich mal loszuwerden, musste ich mal wieder dringend etwas schreiben. Und da ihr mir ja schön regelmäßig neue Fragen zu HTML5 und Co zuschickt, mangelt es mit wahrlich nicht an Material. An die Arbeit!

Asymetrische Addition in JavaScript

Warum ergibt in JavaScript [] + {} als Ergebnis den String "[object Object]", während {} + [] die Zahl 0 liefert?

Das erwartete Ergebnis der Addition eines leeren Objekts und eines leeren Arrays ist immer der String "[object Object]". Dass das bei {} + [] nicht passiert, liegt ganz einfach daran, dass hier gar nicht addiert wird.

Obwohl JavaScript bis Version ECMAScript 6 nur Function Scope gab, existierte der Block als Syntax schon immer. Man kann seit jeher { var x = 42; } schreiben, hat aber nichts viel davon. Wenn wir {} + [] ausführen (d.h. den Code genau so in die Konsole tippen), produzieren wir einen leeren Block und die Anweisung +[] – und das führt dazu, dass das Array in eine Zahl umgewandelt wird. Über den Umweg eines leeren Strings (die Zusammenfassung aller im Array enthaltenen Elemente) spuckt Number() eine 0 aus und zack: {} + [] === 0.

Wenn wir die beiden Schweifklammern in Klammern verpacken, interpretiert sie die JS-Engine nicht mehr als Block und wie bestellt kommt das richtige Ergebnis heraus: ({}) + [] === "[object Object]"

Native Prototypen erweitern – immer böse?

Die Prototypen nativer Objekte zu erweitern gilt als schlechter Stil. Was hältst du von solchen Erweiterungen, die die spezifizierte Methoden von ECMAScript 6 nachbauen? Zum Beispiel gibt es da diesen Poylfill für String.prototype.includes …

Polyfills sind die Ausnahme von der Regel, vorausgesetzt sie setzen den Standard entweder zu 100% korrekt um oder funktionieren auf eine Weise, die später nicht mit der echten Implementierung kollidiert. Einen Polyfill so zu konstruieren ist eine unglaublich anspruchsvolle Aufgabe.

Vor nicht all zu langer Zeit hat ein winziger Fehler mit drastischen Folgen im Polyfill für das Picture-Element für Aufsehen gesorgt und beinahe Umbauten in den echten Implementierungen und dem Picture-Standard selbst verursacht. Solche Dinge passieren, wenn ein Polyfill nicht perfekt ist (weniger als perfekt ist hier nicht akzeptabel) und sich an genau der Stelle einnistet, an der später die echte – möglicherweise subtil andere – Platz zu nehmen versucht.

Wenn ich den in der Frage verlinkten Polyfill mit dem Standard für String.prototype.includes vergleiche, sieht er mir nicht sehr exakt aus. So hat z.B. hat die Funktion im Polyfill nicht die length von 1, die der Standard vorschreibt. Auch fehlt der TypeError, der geworfen werden müsste, wenn der Input-Suchstring ein regulärer Ausdruck ist. Mir persönlich wäre dieser Schnipsel zu riskant, um ihn als Polyfill zu benutzen oder zu veröffentlichen.

Is Service Worker ready?

Ich möchte eine Webapp mit begrenztem Nutzerkreis bauen. Als Browser könnte ich Chrome voraussetzen. Die App muss erkennen, wenn kein Netz mehr vorhanden ist und getätigte Formulareingaben zwischenspeichern, bis der Nutzer wieder verbunden ist. Kann man dafür schon den Service Worker verwenden?

Was Chrome angeht ist die Unterstützung für Service Worker schon recht umfassend. Allerdings scheint mir für das beschriebene Problem der Service Worker gar nicht nötig zu sein. Nur wenn ein Hintergrunddienst laufen oder (u.A. zum Zwecke des Offline-Supports) Requests manipuliert werden sollen, führt kein Weg am Service Worker vorbei. Wenn es aber nur darum geht, Formulareingaben lokal zu speichern (und nicht das Ausliefern des Formulars das zu lösende Problem ist) reicht einer der guten alten HTML5-Speichermechanismen wie Web Storage oder Indexed DB. Hiermit könnte man einfach immer die Formulareingaben speichern ohne sich mit der Offline-Status-Erkennung (im besten Fall knifflig, im schlechtesten Fall unmöglich) herumzuschlagen. Und rund um das Thema „Formulardaten speichern“ gibt es sogar fertige Scripts wie Garlic.js

Zusammengefasst: Ja, für Chrome stünden Service Worker bereit, aber vielleicht reicht ja sogar ein sehr viel älterer Webstandard.

Welche Breakpoints für Responsive Design verwenden?

Ich habe nach Empfehlungen für Breakpoints für Responsive Design gesucht und über 3000 verschiedene, widersprüchliche Empfehlungen. Welche Breakpoints sind denn nun empfehlenswert?

Es gibt nur einen universellen Breakpoint für Responsive Design: den, ab dem es anfängt nicht mehr gut auszusehen. Ab welcher Bildschirmbreite ein Design verändert werden muss, hängt ganz allein vom Design ab – und da das bei jeder Webseite anders ist, gibt es keinen einzelnen Breakpoint der für alle gültig ist. Meine Empfehlung: einfach das machen, was gut aussieht und gut funktioniert.

Weitere Fragen?

All diese Fragen wurden mir per E-Mail, Twitter oder einen anderen digitalen oder analogen Kanal gestellt und auch eure Fragen zu HTML(5), CSS(3), JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach mich anschreiben oder gleich das komplette Erklärbären-Paket kommen lassen.

Ein kleines CSS-Rätsel (mit Auflösung)

Am Freitag twitterte ich ein kleines CSS-Rätsel und bekam so viele verschiedene Antworten, dass ich an dieser Stelle nochmal die ausführliche (richtige) Antwort aufschreiben möchte.

Die Frage dreht sich darum, welche Farbe der Text im folgenden HTML-Schnipsel erhält …

<p class="foo">
  A
</p>

… wenn dieses CSS auf ihn losgelassen wird:

p.foo:not(#baz) {
  color: yellow;
}

p.foo:not(.bar) {
  color: blue;
}

p.foo:not(p#foo) {
  color: magenta;
}

p.foo {
  color: green;
}

.foo {
  color: red;
}

Die meisten Quiz-Teilnehmer tippten auf Magenta und waren erstaunt, in ihren Browsern Gelb zu sehen. Einige wenige tippten auch auf Magenta und sahen es am Ende auch in ihrem Browser. Wie so oft in der Frontend-Entwicklung lautet die richtige Antwort auf dieses Rätsel: es kommt darauf an! Je nachdem welchen Browser man auf den Code loslässt, kommt das eine oder das andere heraus.

Eigentlicher Gegenstand des Rätsels ist die Selektorspezifität. Wären alle Selektoren gleich viel wert, würde die letzte auf ein Element anwendbare CSS-Regel alle anderen überstimmen und der Text würde rot. Doch verschiedene Selektoren haben verschieden viel Gewicht: ID-Selektoren schlagen Klassen-Selektoren, Klassen-Selektoren schlagen Element-Selektoren und kombinierte Selektoren wie z.B. p.foo wiegen einen aus ihren Bestandteilen errechneten Wert (Cheat Sheet). Da unter allen Selektoren im Rätsel der für die rote Farbe der unspezifischste ist, muss der Text eine andere Farbe bekommen. Aber welche?

Die Spezifität eines Selektors setzt sich aus drei Bestandteilen zusammen:

  • A ist die Anzahl aller ID-Selektoren im Selektor
  • B ist die Anzahl aller Klassen-, Attribut- und Pseudoklassen-Selektoren
  • C ist die Anzahl aller Typ- und Pseudoelement-Selektoren

Die :not()-Pseudoklasse selbst trägt nicht zur Spezifität bei, der in ihr enthaltene Selektor hingegen schon. Der Universal-Selektor * wird ignoriert.

Um zu entscheiden ob ein Selektor spezifischer ist als ein anderer werden die drei Komponenten A, B und C verglichen. Der Selektor A mit dem größeren A-Wert ist spezifischer; bei Gleichstand wird B verglichen und herrscht auch hier Gleichstand, wird C verglichen. Bei kompletter Gleichwertigkeit siegt der zuletzt definierte Selektor.

Berechnen wir doch mal (mit z.B. diesem Tool) die Spezifität aller Selektoren im Rätsel und sortieren sie entsprechend ihres Gewichts:

Selektor Spezifität Farbe
p.foo:not(p#foo) (1, 1, 2) Magenta
p.foo:not(#baz) (1, 1, 1) Gelb
p.foo:not(.bar) (0, 2, 1) Blau
p.foo (0, 1, 1) Grün
.foo (0, 1, 0) Rot

Eigentlich klarer Sieg für das Team Magenta, das dank des p#foo in :not() einen Typselektor mehr hat als Gelb. Warum aber erkennt nicht jeder Browser Magenta als Sieger an? Ganz einfach: :not(p#foo) ist erst ab Selectors Level 4 zulässig! In CSS3 kann :not() nur einfache Selektoren (simple selectors, d.h. alleinstehende Typ-, Attribut-, Klassen-, ID-, oder Pseudoklassen-Seletoren) aufnehmen. Der Selektor p#foo ist ein aus zwei Teilen zusammengesetzter compound selector und im Kontext der :not()-Pseudoklasse für alle außer den allermodernsten Browsern unverständlich.

Interessantes Detail am Rande: Selectors Level 4 definiert zwei Selektor-Profile, ein schnelles und ein vollständiges. Wie man erahnen kann, enthält das schnelle Selektor-Profil nicht alle Features des vollständigen und soll vor allem direkt im Browser-Rendering zum Einsatz kommen. Das vollständige Feature-Set ist für die diversen DOM-Selektor-APIs vorgesehen. In beiden Profilen kann die :not()-Pseudoklasse compound selectors verwenden d.h. dieses neue Feature wird universell einsetzbar sein. Nur der Einsatz von komplexen Selektoren, d.h. solchen mit Kombinatoren, ist dem vollständigen Profil vorbehalten.

Warum man die Finger von den Prototypen nativer Objekte zu lassen hat

In regelmäßigen Abständen schlägt bei mir die Frage auf, ob es nicht in ganz bestimmten Fällen doch in Ordnung wäre, die Prototypen der nativen JavaScript- und DOM-Objekte zu erweitern. Meine Antwort darauf ist immer: in öffentlich zugänglichen Projekten, ist das ein absolutes No-Go. Gegen den Einsatz bei privaten Projekten oder bei Spezialtools wie Testframeworks (z.B. should.js) lassen sich auch Argumente finden, aber darüber kann man durchaus zweierlei Meinung sein.

Wenn es darum geht, Gründe gegen das Erweitern der nativen Built-Ins zu finden, wird oft die Kollisionsgefahr zuerst herausgekramt. Würde man in seinem Code Element.prototype.foo definieren, könnte eine Third-Party-Library jetzt oder in Zukunft auch etwas unter Element.prototype.foo implementieren wollen. Diesem Argument lässt sich entgegensetzen, dass das erstens nie passiert und sich zweitens in 99% aller Fälle problemlos reparieren lassen dürfte – einfach das eigene Element.prototype.foo in Element.prototype.foobar umbenennen und schon funktioniert wieder alles. Aber wenn man vom Start weg nicht den Namen Element.prototype.foo, sondern etwas wie Element.prototype.$_foo wählt, werden derartige Reparaturen höchstwahrscheinlich gar nicht erst nötig werden. Auf Projektebene ist die Kollisionsgefahr mit anderen Libraries praktisch nicht gegeben.

Das größere Problem mit dem Erweitern von nativen Objekten besteht nicht auf der Projekt-, sondern auf der Web-Ebene. Browser und Spezifikationen müssen bei der Entwicklung neuer Features immer darauf achten, dass die Neuheiten kompatibel zum gesamten existierenden, öffentlichen HTML/JavaScript/CSS-Code sind. Sie haben es also mit der denkbar größten Legacy-Codebase der Welt zu tun, nämlich dem kompletten WWW. Das macht die Entwicklung von Neuheiten recht knifflig, nicht zuletzt weil einige Entwickler nicht die Finger von den Prototypen nativer Elemente lassen konnten. Das beste Beispiel dafür ist die in ECMAScript 6 neu eingeführte Funktion Array.prototype.includes(), die prüft ob ein Array ein gegebenes Element enthält. Ursprünglich sollte das neue Feature den Namen Array.prototype.contains() tragen, doch es stellte sich heraus, dass mehrere Webseiten bereits länger eine genau so benannte, aber subtil anders implementierte Funktion gleichen Namens verwenden. Diese Funktion stammt aus der JavaScript-Library Mootools, die schon vor Jahren den Zenit ihrer Popularität überschritten hatte. Aber da es immer noch Webseiten gibt, die Mootools in der einen oder anderen Version verwenden, war der Name Array.prototype.contains() für ECMAScript 6 nicht zu gebrauchen. Es erfolgte die Umbenennung in includes() und in Folge musste natürlich auch die entsprechende String-Methode für ES6 umbenannt werden.

Das Beispiel zeigt, was passieren kann, wenn man als normaler Webentwickler die Prototypen von nativen Built-Ins erweitert: man fuhrwerkt im Legacy-Code jeder Browserengine und jeder Spezifikation herum, die es jemals gab und jemals geben wird! Und da man nicht weiß, in welche Richtung die Entwicklung der Webstandards in 3 Jahren gehen wird, sollte man tunlichst die Finger von den entsprechenden Objekten lassen. Vielleicht wird es nie ein Problem sein, sein eigenes Element.prototype.$_foo zu definieren. Vielleicht wird man genau damit aber auch ein Feature von DOM 6.0 oder ECMAScript 12 blockieren.

Zusammengefasst ist es in Ordnung, die nativen Prototypen zu erweitern, wenn entweder

  1. das Projekt privat ist (z.B. Intranet-App, Testing-Tool) und die entsprechenden Codezeilen auch niemals das WWW betreten werden, und sei es durch Copy & Paste in ein neues Projekt
  2. oder man sicher weiß, dass der entsprechende Code niemals mit irgendeinem zukünftigen Standard kollidieren wird oder dass zu dem Zeitpunkt, an dem die Kollision geschehen wird, garantiert vom fraglichen Code keine einzige Zeile mehr irgendwo im WWW aufzufinden ist.

Ich weiß nicht, wie man diese Bedingungen erfüllen will, ohne Hellseher zu sein. Daher lasse ich die Finger von den Prototypen der nativen Built-Ins und empfehle das auch jedem, der mich fragt.

Folgt mir!

Kauft mein Zeug!

Cover der HTML5-DVD
Infos auf html5-dvd.de, kaufen bei Amazon

Cover des HTML5-Buchs
Infos auf html5-buch.de, kaufen bei Amazon

Cover des CSS3-Buchs
Infos auf css3-buch.de, kaufen bei Amazon