Entwurf: Houdini - entfesseltes CSS. Teil 2: das CSS Typed Object Model
Rolf B
- css
- houdini
- javascript
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:
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.
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.
Basis des Typed OM ist das StylePropertyMap
Objekt, bzw. seine readonly-Variante StylePropertyMapReadOnly
. Sie können solche Objekte an drei Stellen erhalten:
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
.
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.
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.
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.