Script-Elemente von einem Dokument ins nächste importieren

Veröffentlicht am 8. September 2016

Dieser Artikel befasst sich mit einem Problem aus den Randbezirken der täglichen Praxisrelevanz, aber da ich daran ganz schön herumtüfteln musste, kann die Nachwelt sich vielleicht nach der Lektüre der nächsten paar Zeilen ein wenig Arbeit ersparen. Das Problem tauchte auf, als ich einen Bug in meiner kleinen Web Component namens html-import bemerkte. Die Komponente verwende ich, um in meinen HTML-basierten Präsentationen eine clientseitige Importfunktion umzusetzen, ohne dass ich JavaScript zu schreiben brauche. In einer gegebenen Präsentation A möchte ich buchstäblich <html-import src="praesentationB.html#SlideIdFoo"> schreiben können, statt Copy & Paste zu betreiben. Die Komponente funktioniert wie folgt:

  1. Die im src-Attribut angegebene URL wird mit fetch() geladen
  2. Der HTML-Content wird in ein neues Dokument geparsed
  3. Entweder der komplette Inhalt des Dokuments oder ein bestimmtes Element werden in das importierende Dokument importiert (document.importNode())
  4. Der importierte Inhalt wird im Dokument nach dem importierenden <html-import>-Element eingehängt

Dazu kommt allerlei Hexerei für ein Promise auf dem <html-import>-Element und rekursive Imports. Und alles funktionierte ganz hervorragend, bis mir auffiel, dass importierte <script>-Elemente im Firefox nicht ausgeführt wurden, während sie in Chrome funktionierten …

Nach längerer Recherche kann ich nur vermuten, dass Firefox ganz einfach aus Sicherheitsgründen streikt. Ich habe beim Durchforsten der HTML5- und DOM-Spezifikationen nicht herausgefunden, ob das vorgeschrieben oder zulässig ist, aber es scheint einfach so zu sein, dass der Firefox das aus einem fremden Dokument importierte Script nicht vertrauenswürdig findet. Was also tun?

Firefox hat ein Problem mit dem importierten Script-Element, aber nicht mit dem Script-Inhalt. Die Lösung besteht also darin, ein neues Script-Element im importierenden Dokument zu erstellen, den Script-Inhalt (mit der text-Property, nicht mit innerHTML) und/oder das src-Attribut vom Original im Fremd-Dokument zu übernehmen und das neue Script nach dem Original im Original-Dokument einzufügen. Dann funktioniert das Script im Firefox, läuft aber in Chrome zweimal (denn dort funktionieren Original und Klon gleichermaßen). Hier gibt es nun zwei Möglichkeiten:

  1. In meinem Fall, in dem ich Inhalt aus einem Fremd-Dokument übernehme, kann ich ganz einfach das Original-Script aus dem Dokument heraushalten. Nach dem Anlegen des Klons wird das Original einfach verworfen statt ins Dokument eingehängt.
  2. Falls das mal nicht so einfach möglich sein sollte, kann man das Original-Script auch deaktivieren. Hierzu einfach als type-Attribut etwas angeben, das nicht auf der HTML5-Liste der Script-MIME-Types steht.

Das neue Script-Element wird asynchron ausgeführt, aber das wäre beim direkten Import des Originals nicht anders.

Wie sich die ganze Angelegenheit in IE und Safari o.Ä. darstellt, habe ich noch nicht getestet. Auffällig fand ich, dass das für mich das erste Mal seit langem war, dass ich eine Frage nicht durch irgendeine Spezifikation zumindest teilweise beantwortet bekommen habe. Normalerweise kann man sich heutzutage darauf verlassen, dass man in HTML5 u.Ä. zumindest so etwas wie einen klar definierten Soll-Zustand vorfindet. Empirische Browser-Forschung ist jedenfalls bei mir zum Ausnahmefall geworden. Das war früher mal gaaanz anders …

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

Veröffentlicht am 23. August 2016

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

Veröffentlicht am 29. Juni 2016

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)

Veröffentlicht am 9. November 2015

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.