Rolf B: Houdini - entfesseltes CSS. Teil 2: das CSS Typed Object Model

Die CSS Houdini Initiative existiert seit einigen Jahren, um CSS und JavaScript einander näher zu bringen. Von den geplanten sieben APIs sind mittlerweile drei in Browsern mit Blink-Engine (Chromium) implementiert, Webkit (Safari) arbeitet daran und Gecko (Firefox) denkt darüber nach.

Zeit also, sich diese APIs einmal näher anzuschauen.

Bisherige Artikel in der Reihe:

Welche Browser?

Jetzt, November 2021, wird das CSS Typed OM nur von den Chromium Browsern unterstützt. Apple entwickelt für Safari noch daran, und Mozilla arbeitet zwar an der Spec mit, findet aber bislang, dass das Thema "einen Prototyp wert wäre", wobei beim Typed OM sogar jemand meinte, es auf "kann nicht schaden" herabzustufen.

Und ganz unrecht hat dieser Jemand nicht - auch wenn es jemand von Google war. Das Typed Object Model ist ein Houdini-Aspekt, auf den man in vielen Fällen gut verzichten kann. Tab Atkins (auch von Google) hat allerdings ein paar Benchmarks gemacht und gefunden, dass man mit dem Typed OM bis zu 30% schneller sein kann.

Auch bei Chrome ist man noch nicht fertig. Sie unterstützen bisher nur einen Teil aller CSS Propertys. Nur, welchen Teil, das verstecken sie hinter einer Login-Mauer.

Und auch die Spec ist unfertig. Alles, was mit Bildern zu tun hat, wird auf einen späteren Level verschoben.

Wir haben doch schon ein CSS Objektmodell

Nun ja, sicher, die CSSOM-Spezifikation. Es gibt unter document.stylesheets die geladene StyleSheetList, von dort aus hangelt man sich zu CSSStyleSheet, CSSRule und CSSStyleDeclaration durch. Eine CSSStyleDeclaration finden wir auch in der style-Eigenschaft von HTML Elementen oder bekommen sie von window.getComputedStyle.

Das ist alles großartig, um Stylesheets mittels JavaScript aufzubauen. Man kann ein leeres style-Element im head unterbringen, man kann ihm diverse Unterklassen von CSSRule-Objekten hinzufügen und diesen Rules Selektoren und Eigenschaften geben.

Anders sieht es aus, wenn man die Werte der so gesetzten Eigenschaften wieder auslesen will. Das CSSOM ist ausschließlich auf Strings aufgebaut. Wenn Sie dort den Wert "17px" vorfinden, müssen Sie die 17 vom "px" trennen, um den Zahlenwert zu erhalten. Das heißt: Der Browser muss, um Ihnen die vom CSSOM vorgegebene Darstellung zu liefern, seinen internen numerischen Wert in einen String verwandeln und ein "px" anhängen, nur damit Sie es wieder auseinandernehmen können. Diesen Überbau kann man mit dem Typed OM verringern.

Wie verwendet man das Typed OM?

Basis des Typed OM ist das StylePropertyMap Objekt, bzw. seine readonly-Variante StylePropertyMapReadOnly. Sie können solche Objekte an drei Stellen erhalten:

  • von einem HTML Element mit der computedStyleMap Methode, dies liefert die readonly-Variante und entspricht der window.getComputedStyle Methode
  • von einem HTML Element mit der attributeStyleMap Methode, dies liefert die readwrite-Variante und entspricht dem Zugriff auf das style-Property des Elements
  • von einem CSSRule-Objekt über das styleMap Property, auch dies liefert die readwrite-Variante.

Die erste dieser drei Möglichkeiten ermittelt den aktuellen Zustand der CSS Eigenschaften eines HTML Elements, während die beiden anderen die über Stylesheet und style-Attribut deklarierten Werte zur Verfügung stellen.

<div id="aDiv" style="margin-left: auto; margin-right: 1em;
                      margin-top: calc(1px + 2em)">
   ...
</div>
const aDiv = document.getElementById("aDiv");
const aDivSM = aDiv.attributeStyleMap;
const lMargin = aDivSM.get("margin-left");
const rMargin = aDivSM.get("margin-right");
const tMargin = aDivSM.get("margin-top");
const theMargin = aDivSM.get("margin");

In diesem kleinen Einstiegsbeispiel wird die attributeStyleMap-Eigenschaft des div-Elementobjekts benutzt, um auf das style-Attribut dieses div Elements mit dem Typed OM zuzugreifen.

Deutlichster Unterschied zum bisherigen CSSOM ist, dass es keine eigenen Propertys für die diversen CSS Eigenschaften gibt. Statt dessen verwenden Sie die Methode get, um mit exakt dem Namen auf die Eigenschaften zuzugreifen, mit denen sie auch in der Style-Angabe vorkommen.

Den zweite Unterschied stellen Sie fest, wenn Sie sich die Rückgabewerte von get anschauen. Es sind keine Strings, statt dessen sind es Objekte, vom Typ CSSStyleValue. Schlechtestenfalls. CSSStyleValue ist die Basisschnittstelle für Werte von CSS Eigenschaften und kommt dann zum Einsatz, wenn das Typed OM nicht wirklich etwas mit dem Eigenschaftswert anzufangen weiß.

Im Falle von lMargin und rMargin bekommen wir etwas anderes. In lMargin finden wir ein Objekt mit der CSSKeywordValue-Schnittstelle, in rMargin ein Objekt mit der CSSUnitValue-Schnittstelle, und in tMargin etwas ganz Verrücktes: CSSMathSum.

Die Vielfalt dieser möglichen Werte ist in der Vielfalt der möglichen CSS-Angaben begründet. Wir verwenden die attributeStyleMap, die die Deklaration der Eigenschaft wiedergibt. Und wir bekommen hier in objektorientierter Form genau das angeboten, was im Style steht.

Ein CSSKeywordValue hat lediglich eine value-Eigenschaft, worin sich der Name des Keywords findet. Im Falle unserer margin-left Eigenschaft also "auto". Aber das ist tatsächlich mehr als nur aDiv.style.marginLeft abzufragen, denn den Inhalt der lMargin Variablen kann ich mit instanceOf CSSKeywordValue direkt prüfen, ob die CSS Eigenschaft ein Schlüsselwort enthält.

Der CSSUnitValue, den wir für margin-right erhalten, besitzt schon zwei Eigenschaften: value und unit. In value finden Sie bei CSSUnitValue Objekten immer eine Zahl, und in unit die verwendete Einheit. Das sind die bekannten CSS Einheiten wie 'px', 'mm' oder 'em', es kann aber auch ein Prozentwert sein, die Einheit lautet dann 'percent'. Zu CSSUnitValues später mehr.

Das CSSMathSum Objekt, das wir für margin-top erhalten, repräsentiert den calc-Ausdruck. Es ist ein CSSMathSum-Objekt, weil die höchstrangige Operation im calc-Ausdruck eine Addition ist. Es besitzt eine Eigenschaft operator - hier mit dem Wert "sum" - sowie eine Eigenschaft values, die ein Array mit den verwendeten Operanden enthält. In unserem Beispiel wären das zwei CSSUnitValues, einer für 1px, der andere für 2em.

Das ist aber kompliziert

Nun ja. CSS ist auch kompliziert. Im Fall der attributeStyleMap ist es aber vor allem auch deshalb kompliziert, weil das die deklarierten Werte sind, und nicht die echten Werte, wie man sie auch von window.getComputedStyle erhalten würde.

Wenn Sie aDiv.computedStyleMap() aufrufen - ja, diesmal ist es eine Methode, keine Eigenschaft - erhalten Sie ein StylePropertyMapReadOnly Objekt. Hier finden Sie keine relativen Einheiten wie em oder %, sondern nur noch Pixel. Mit einer Ausnahme: margin-right war im oben stehenden Beispiel 'auto' - und dieser Wert wird nicht als realer Pixelwert geliefert. Das ist ein Unterschied zu window.getComputedStyle, das Ihnen den Wert liefert, den der Browser für auto berechnet hat.

Zahlen. Nur Zahlen?

Nein, nicht nur. Es gibt auch schlimmeres. Wenn Sie beispielsweise ein custom property verwenden, ohne es zu deklarieren (siehe Teil 1 dieser Reihe), bekommen Sie den nackten Text in Form eines CSSUnparsedValue. Deklarieren Sie es korrekt über eine @property Angabe, erhalten Sie für Längenangaben einen CSSUnitValue.

Aber auch mit deklarierten custom properties ist das TypedOM widerspenstig. In der Spezifikation wurde aufgeschrieben, dass alle CSS Werte, die custom properties verwenden (also eine var(--...) Angabe enthalten), in der CSSStyleMap zu CSSUnparsedValue Objekten werden. Das klingt verrückt, aber...

div {
   --bgImg: linear-gradient(120deg, black 0%, white 100%);
   --bgPosSizeX: center / 50px;
   background: var(--bgImg) var(--foo) 100px no-repeat;
};

Ihnen wird schlecht? Den Typed OM Designern wohl ebenfalls, als ihnen klar wurde, was die Spezifikation der custom properties ihnen da eingebrockt hat. Man kann nicht nur mehrere Teil-Eigenschaften für background aus einem custom property holen, man kann es sogar soweit treiben, dass nur ein Teil eines Eigenschaftswertes im custom property steht und der andere Teil direkt angegeben wird.

Angesichts der chaotischen Varianten, die damit möglich werden, ist kein Werteobjekt für eine solche Sammeleigenschaft denkbar, die diese CSS Angabe repräsentiert. Deswegen verzichtet das TypedOM auch ganz darauf, und bietet nur den Zugriff auf Einzeleigenschaften wie background-image, background-position oder background-size.

Weil aber ein var-Wert über mehrere Einzeleigenschaften streuen kann, ist auch eine Zuordnung zu den Einzeleigenschaften nicht möglich.

Fummelei mit Style-Werten

Um zu verstehen, was das Typed OM leisten kann, schauen wir uns ein etwas exotischeres Beispiel an.

<div class="sprite" style="top:15mm; left: 220mm">😀</div>
let sprite = ... /* Sprite beschaffen */
let top = sprite.style.top;
let left = sprite.style.left;

Und nun? Wir haben ein div, das nicht im normalen Flow der Seite lebt, sondern auf Millimeterbasis positioniert wird, und wir möchten mit der Position etwas anfangen. Wir haben nun die Aufgabe, aus den Style-Werten die Einheit zu entfernen - was eine String-Operation ist, müssen den numerischen Teil des Strings in eine Zahl umwandeln.

Das Typed Object Model der Houdini-Initiative bietet hier eine Alternative an:

let sprite = ... /* Sprite beschaffen */
let spriteTop = sprite.attributeStyleMap.get("top");
let spriteLeft = sprite.attributeStyleMap.get("left");
console.log(spriteTop);

Was wir jetzt in top und left vorfinden, ist kein String mehr, sondern ein Objekt vom Typ CSSUnitValue mit - wenig überraschend - den Eigenschaften value und unit. In der value Eigenschaft finden wir den Zahlenwert, in unit den Namen der Einheit. Im gezeigten Beispiel würde spriteTop als

CSSUnitValue {value: 15, unit: 'mm'}

geloggt werden.