Auch im letzten Monat haben lautes Nachdenken und lebhafter Austausch auf Mastodon dazu geführt, dass ich einiges über HTML, CSS und JavaScript/DOM erfahren habe, das mir vorher nicht klar war. Und da Mastodon noch immer nicht die intergalaktische Total-Dominanz ausübt, die ihm eigentlich zusteht, kehre ich die gesammelten Erkenntnisse an dieser Stelle nochmals zusammen. Das behalte ich ab nun auch bei, bis ihr alle mir dort folgt.

Formulare in Formularen? Jain! (und mit Browserbugs)

Ich habe schon vor langer Zeit mal einen Workflow für Form-Value-Handling in Formular-Web-Components ausgebrütet, der darauf basierte, im Shadow DOM der Komponente ein inneres <form>-Element zu haben. Dieses Element lässt sich zu FormData serialisieren, was dann wiederum bequem in Submit-Values, value-Attribute und alle sonstigen für die Komponente relevanten Aspekte transformiert werden kann. Im Firefox funktionierte das auch hervorragend, aber bei einigen Komponenten streikte Chrome. Warum? Weil mein Form-Handling-Workflow ungültiges HTML verwendet (wenn man denn HTML verwendet).

Die HTML-Standards verbieten verschachtelte Form-Elemente und (was ich nicht auf dem Schirm hatte) Formulare in Shadow Roots in Formularen gelten als verschachtelte Form-Elemente! Allerdings besteht diese Regel auch nur für HTML, nicht für das DOM. Der folgende Code resultiert in nur einem <form>-Element, da der HTML-Parser das innere Element verwirft:

<form>
  <form></form>
</form>

<!-- Ergebnis: ein <form> im DOM -->

Wenn wir aber gar keinen HTML-Parser involvieren, sondern per JS direkt das DOM manipulieren, erhalten wir verschachtelte Formulare:

let outer = document.createElement("form");
let inner = document.createElement("form");
outer.append(inner);
document.body.append(outer);

// Ergebnis: zwei <form> im DOM

Das ist auch nicht so besonders bizarr: HTML ist nur eine Serialisierung des DOM und hat daher die Freiheit, sich bestimmter DOM-Konstrukte zu verweigern, genau wie JSON mit zahlreichen Aspekten von JavaScript nichts anfangen kann.

Mein Komponenten-Fail in Chrome kam dadurch zustande, dass ich bei den Problem-Komponenten innerHTML für das Shadow-DOM-Setup verwendet habe (und andere DOM-Tools bei den unproblematischen Komponenten). innerHTML verwendet natürlich seinerseits den HTML-Parser, der allerdings offenbar in meinem Haupt-Browser Firefox den Verschachtelungs-Überblick verliert, sobald Shadow DOM involviert ist. Bedeutet: im Firefox funktioniert etwas, das laut HTML-Standard nicht funktionieren dürfte. Endlich kann ich mal einen Bug melden, der nicht einfach nur Gebettel um eine Implementierung von Feature X ist!

Meinen Web-Component-Ansatz mit inneren Formularen werde ich beibehalten, obwohl er sich nicht in HTML serialisieren lässt. Solange die inneren Formulare im Shadow DOM bleiben, stören sie nicht, und solange das Shadow DOM ohne einen (bugfreien) HTML-Parser aufgesetzt wird, sollten sie auch funktionieren. Und ich denke nicht, dass deklaratives Shadow DOM ein sinnvolles Einsatzgebiet für Custom Formular-Inputs sein wird, weswegen ich mir erlaube, die Regeln von HTML an dieser Stelle zu ignorieren.

Kompatibilitätsprobleme von CompressionStreams (und deren Zubehör)

Nachdem ich im Januar CompressionStreams über den grünen Klee gelobt hatte, fielen mir im Folgemonat einige Kompatibilitätsprobleme auf. Seit dem LTS-Release von Node 20 herrscht in Hinblick auf die Kompressionsalgorithmen durch die Bank die gleiche Unterstützung, aber Chrome und Chrome-Derivate implementieren nicht @@asyncIterator auf ReadableStream, sodass für diese Browser folgender Polyfill benötigt wird:

ReadableStream.prototype[Symbol.asyncIterator] ??= async function* () {
  const reader = this.getReader();
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        return;
      }
      yield value;
    }
  } finally {
    reader.releaseLock();
  }
};

TypeScript-kompatible Versionen der compress()- und decompress()-Funktionen aus dem Artikel, mit dem o.g. Polyfill und besserem Error Handling und URL-sicherem Base64 gibt es in meiner Toolsammlung.

Die Reihenfolge von Adopted Stylesheets ist egal

Mit new CSSStyleSheet() erstellte Stylesheet-Objekte können der adoptedStyleSheets-Property eines Shadow Root (oder eines Document) zugewiesen werden, um dem entsprechenden Objekt ein bisschen Style überzuhelfen. adoptedStyleSheets kommt als Array daher und trotzdem ist – für mich überraschend – der Array-Index eines gegebenen Stylesheets für die CSS-Anwendung irrelevant. Es zählt allein die Reihenfolge des Hinzufügens:

function createSheet(css) {
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(css);
  return sheet;
}

const host = document.querySelector(".host");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = "<span>Text</span>"
shadow.adoptedStyleSheets[1] = createSheet("span { color: red }");
shadow.adoptedStyleSheets[0] = createSheet("span { color: green }");
// Ergebnis: grün

Ich hätte mich nicht gewundert, wenn der Text rot geworden wäre, da im Array color: red die letzte Regel ist. Aber da sie zuerst hinzugefügt wurde, gewinnt Grün.

Safari bleibt der neue IE6: kein d in CSS

Ich musste entsetzt zur Kenntnis nehmen, dass Safari d in CSS nicht unterstützt – als einziger relevanter Browser diesseits der Andromeda-Galaxie. Eigentlich ist d ein Attribut des SVG-Elements <path>, das den zu zeichnenden Pfad beinhaltet. Wie so ziemlich jedes SVG-Attribut (fill, stroke etc.) kann auch d als CSS-Eigenschaft ausgedrückt werden und d wird in dieser Rolle mit einem path()-Wert gefüttert, genau wie u. a. clip-path. Einziges Problem: Safari mag d in CSS nicht.

Das ist ziemlich verheerend, da damit ein CSS-Manöver verhindert wird, für das ich mich ansonsten ziemlich gefeiert hätte: per Custom Properties konfigurierbare Inline-SVGs! Jetzt muss ich mich damit begnügen, ein endliches Set von vordefinierten d-Werten über Bande per Custom Property Toggles bereitzustellen. Ein ziemlich enttäuschendes Downgrade.

Weitere Erkenntnisse und Fundstücke