Ich nutze meine Mastodon-Präsenz vor allem, um dumme Fragen zu stellen und laut nachzudenken. Fragenstellen und Nachdenken führt zu Erkenntnisgewinn (in meinem Fall meist in Sachen Browserbugs und Webstandards) und dieser Artikel fasst meine Erkenntnis-Highlights aus dem Januar in etwas organsierterer Form zusammen. Top-Fundstück des Monats waren definitiv die bereits in einem eigenen Artikel verarbeiteten CompressionStreams, aber der Rest kann sich auch sehen lassen!

TextEncoder und TextDecoder

Mir war neu, dass alle Browser unter der Sonne TextEncoder und TextDecoder unterstützen. Der Decoder schluckt Bytes und produziert Strings, der Encoder macht das Gegenteil:

const utf8bytes = new TextEncoder().encode("????");
// > Uint8Array(4) [ 240, 159, 164, 161 ]
const string = new TextDecoder().decode(utf8bytes);  
// > "????"

Der Decoder kann natürlich auch mit mehr als UTF-8 umgehen und lief mir beim Austüfteln von CompressionStreams über den Weg.

Kein @import im CSSStyleSheet()-Constructor (und CSS-Modulen)

Ein Ansatz für CSS in Shadow DOM besteht darin, mit new CSSStyleSheet() Stylesheets aus heißer Luft zu erzeugen und diese einem ShadowRoot (oder Document) zuzuweisen:

const host = document.querySelector("div");
const root = host.attachShadow({ mode: "open" });
root.innerHTML = `Hello`;
const sheet = new CSSStyleSheet();
sheet.replaceSync(":host { color: red }"); // async-Alternative: replace()
root.adoptedStyleSheets.push(sheet);

Die in diesem Stylesheet enthaltenen Regeln gelten dann ausschließlich in den ShadowRoots oder Dokumenten, deren adoptedStyleSheets sie zugewiesen wurden. Zwar handelt es sich hierbei um grundsätzlich zweifelhafte JS-Hexerei auf CSS-Territorium, aber es mangelt nicht an Vorteilen:

  • Web Components können ihre eigenen kleinen CSS-Dateien haben (anstelle von Strings in JavaScript)
  • Wer sich etwas Mühe gibt, Memory Leaks zu umgehen, kann ein CSSStyleSheet-Objekt über mehrere Komponenten-Instanzen teilen
  • Unter Zuhilfenahme von Build-Tools kann CSS zur Compile-Zeit ins JavaScript gebundlet werden, falls das gewünscht ist

Allerdings musste ich feststellen, dass @import in mit new CSSStyleSheet() und CSS-Modulen genutzten Stylesheets nicht funktioniert.. Das Problem ist, dass jedes @import traditionell ein Stylesheet von einer URL lädt und für mehrere Requests auf die gleiche Adresse komplett unterschiedliches CSS geliefert bekommen kann. Für ECMAScript-Module hingegen baut der Browser einmal einen Modul-Graph auf und lädt keine URL zweimal – zwei unterschiedliche Antworten innerhalb eines Ladezyklus sind also ausgeschlossen. CSS-Module wollen Syntax wie import css from './foo.css' ermöglichen, doch hier kollidiert die Funktionsweise von ECMAScript-Modulen mit der von @import. Wir erwarten von import-Statements deterministische Ergebnisse und von @import-Regeln das genaue Gegenteil. Die erwartbare Konsequenz: kein @import in CSS-Modulen und auch kein @import in mit new CSSStyleSheet() erzeugten Stylesheets, in denen sich ein vergleichbarer Widerspruch manifestiert.

type auf <textarea> und <select> (und mehr)

Beim Zusammenstecken einiger Debug-Strings fiel mir auf, dass auf <textarea> und <select> das IDL-Attribut type existiert:

const textarea = document.createElement("textarea");
console.log(textarea.type); // > "textarea"

const select = document.createElement("select");
console.log(select.type); // > "select-one"

const multiSelect = document.createElement("select");
multiSelect.multiple = true;
console.log(multiSelect.type); // > "select-multiple"

Anders als bei <input>, wo das Content-Attribut type den Input-Typ bestimmt, ist das IDL-Attribut type bei <textarea> und <select> read-only. Die Idee dahinter scheint zu sein, dass alle Formular-Elemente eine einheitliche API zum Ermitteln ihres Typs haben sollen, denn auch <output> und <fieldset> haben dieses Feature. Unter den verbliebenen form-associated elements haben <input>, <button> und <object> ohnehin type-Attribute und die einzigen Ausreißer sind <img> (warum ist das überhaupt form-associated?) und eventuelle form-associated custom elements. Was lernen wir daraus?

  1. Formular-Elements können wir allein anhand ihres type auseinanderhalten.
  2. Wenn wir form-assoicated custom elements bauen, sollten sich diese auch die Mühe machen, einen type-Getter zu implementieren, denn sonst funktioniert Punkt 1 nicht mehr.

Punkt 2 ist schon erfüllt, wenn wir einfach nur den folgenden Codeschnipsel in unsere Form-Element-Basisklassen einbauen:

export class FormBaseElement extends HTMLElement {
  // Boilerplate...
  
  get type() {
    return this.tagName.toLowerCase();
  }
  
  // ... mehr Boilerplate...
}

Damit funktioniert type praktisch wie bei <textarea> und erfüllt damit in 99% aller Fälle schon locker seinen Zweck!

Weitere Erkenntnisse und Fundstücke

Folgt mir auf Mastodon, wenn ihr dem nächsten Erkenntnis-Paket live beim Entstehen zusehen wollt!