Mit dem neuen CSS Typed-Objektmodell arbeiten

Kurzfassung

CSS hat jetzt eine objektbasierte API für die Arbeit mit Werten in JavaScript.

el.attributeStyleMap.set('padding', CSS.px(42));
const padding = el.attributeStyleMap.get('padding');
console.log(padding.value, padding.unit); // 42, 'px'

Die Zeiten, in denen Sie Strings verketten mussten und es zu subtilen Fehlern kommen konnte, sind vorbei.

Einführung

Altes CSSOM

CSS hat seit vielen Jahren ein Objektmodell (CSSOM). Tatsächlich verwenden Sie .style immer, wenn Sie es in JavaScript lesen oder festlegen:

// Element styles.
el.style.opacity = 0.3;
typeof el.style.opacity === 'string' // Ugh. A string!?

// Stylesheet rules.
document.styleSheets[0].cssRules[0].style.opacity = 0.3;

Neues CSS Typed OM

Das neue CSS Typed Object Model (Typed OM), das Teil des Houdini-Projekts ist, erweitert diese Sichtweise durch das Hinzufügen von Typen, Methoden und einem geeigneten Objektmodell für CSS-Werte. Anstelle von Strings werden Werte als JavaScript-Objekte bereitgestellt, um eine leistungsstarke (und sinnvolle) Bearbeitung von CSS zu ermöglichen.

Anstelle von element.style greifen Sie über die neue .attributeStyleMap-Eigenschaft für Elemente und die .styleMap-Eigenschaft für Stylesheet-Regeln auf Styles zu. Beide geben ein StylePropertyMap-Objekt zurück.

// Element styles.
el.attributeStyleMap.set('opacity', 0.3);
typeof el.attributeStyleMap.get('opacity').value === 'number' // Yay, a number!

// Stylesheet rules.
const stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].styleMap.set('background', 'blue');

Da StylePropertyMaps Map-ähnliche Objekte sind, unterstützen sie alle üblichen Verdächtigen (get/set/keys/values/entries), was sie flexibel macht:

// All 3 of these are equivalent:
el.attributeStyleMap.set('opacity', 0.3);
el.attributeStyleMap.set('opacity', '0.3');
el.attributeStyleMap.set('opacity', CSS.number(0.3)); // see next section
// el.attributeStyleMap.get('opacity').value === 0.3

// StylePropertyMaps are iterable.
for (const [prop, val] of el.attributeStyleMap) {
  console.log(prop, val.value);
}
// → opacity, 0.3

el.attributeStyleMap.has('opacity') // true

el.attributeStyleMap.delete('opacity') // remove opacity.

el.attributeStyleMap.clear(); // remove all styles.

Beachten Sie, dass im zweiten Beispiel opacity auf „string“ ('0.3') gesetzt ist, aber später beim Lesen des Attributs eine Zahl zurückgegeben wird.

Vorteile

Welche Probleme sollen mit dem CSS Typed OM gelöst werden? Wenn Sie sich die obigen Beispiele (und den Rest dieses Artikels) ansehen, werden Sie vielleicht feststellen, dass das CSS Typed OM viel ausführlicher ist als das alte Objektmodell. Ich stimme zu.

Bevor Sie Typed OM abschreiben, sollten Sie sich einige der wichtigsten Funktionen ansehen:

  • Weniger Fehler: Numerische Werte werden immer als Zahlen und nicht als Strings zurückgegeben.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Arithmetische Operationen und Einheitenumrechnung: Umrechnung zwischen absoluten Längeneinheiten (z. B. px –> cm) und grundlegende mathematische Berechnungen.

  • Werte begrenzen und runden Bei typisierten OM-Variablen werden Werte gerundet und/oder begrenzt, damit sie innerhalb der zulässigen Bereiche für ein Attribut liegen.

  • Bessere Leistung: Der Browser muss weniger Arbeit für die Serialisierung und Deserialisierung von Stringwerten leisten. Die Engine verwendet jetzt ein ähnliches Verständnis von CSS-Werten in JS und C++. Tab Akins hat einige frühe Leistungsbenchmarks veröffentlicht, die zeigen, dass Typed OM bei Operationen pro Sekunde etwa 30% schneller ist als das alte CSSOM mit Strings. Das kann bei schnellen CSS-Animationen mit requestionAnimationFrame() von Bedeutung sein. Unter crbug.com/808933 finden Sie weitere Informationen zu Leistungsverbesserungen in Blink.

  • Fehlerbehandlung: Neue Parsing-Methoden ermöglichen die Fehlerbehandlung in CSS.

  • „Sollte ich CSS-Namen oder ‑Strings in CamelCase verwenden?“ Sie müssen nicht mehr raten, ob Namen im CamelCase-Format oder als Strings angegeben werden (z.B. el.style.backgroundColor im Vergleich zu el.style['background-color']). CSS-Eigenschaftsnamen im Typed OM sind immer Strings, die dem entsprechen, was Sie tatsächlich in CSS schreiben :)

Browserunterstützung und Funktionserkennung

Die Eingabe-OM wurde in Chrome 66 eingeführt und wird in Firefox implementiert. Edge hat Unterstützung signalisiert, aber noch nicht in sein Plattform-Dashboard aufgenommen.

Für die Erkennung von Funktionen können Sie prüfen, ob eine der numerischen CSS.*-Fabriken definiert ist:

if (window.CSS && CSS.number) {
  // Supports CSS Typed OM.
}

API-Grundlagen

Auf Stile zugreifen

Werte sind im CSS Typed OM von Einheiten getrennt. Wenn Sie einen Stil abrufen, wird ein CSSUnitValue mit einem value und einem unit zurückgegeben:

el.attributeStyleMap.set('margin-top', CSS.px(10));
// el.attributeStyleMap.set('margin-top', '10px'); // string arg also works.
el.attributeStyleMap.get('margin-top').value  // 10
el.attributeStyleMap.get('margin-top').unit // 'px'

// Use CSSKeyWorldValue for plain text values:
el.attributeStyleMap.set('display', new CSSKeywordValue('initial'));
el.attributeStyleMap.get('display').value // 'initial'
el.attributeStyleMap.get('display').unit // undefined

Berechnete Stile

Berechnete Stile wurden von einer API auf window zu einer neuen Methode auf HTMLElement verschoben, computedStyleMap():

Altes CSSOM

el.style.opacity = 0.5;
window.getComputedStyle(el).opacity === "0.5" // Ugh, more strings!

Neues Typed OM

el.attributeStyleMap.set('opacity', 0.5);
el.computedStyleMap().get('opacity').value // 0.5

Begrenzung / Rundung von Werten

Eine der praktischen Funktionen des neuen Objektmodells ist das automatische Begrenzen und/oder Runden von berechneten Stilwerten. Angenommen, Sie versuchen, opacity auf einen Wert außerhalb des zulässigen Bereichs [0, 1] festzulegen. Bei typisierten OM-Klammern wird der Wert bei der Berechnung des Stils auf 1 begrenzt:

el.attributeStyleMap.set('opacity', 3);
el.attributeStyleMap.get('opacity').value === 3  // val not clamped.
el.computedStyleMap().get('opacity').value === 1 // computed style clamps value.

Wenn Sie z-index:15.4 festlegen, wird der Wert auf 15 gerundet, sodass er eine Ganzzahl bleibt.

el.attributeStyleMap.set('z-index', CSS.number(15.4));
el.attributeStyleMap.get('z-index').value  === 15.4 // val not rounded.
el.computedStyleMap().get('z-index').value === 15   // computed style is rounded.

Numerische CSS-Werte

Zahlen werden im Typed OM durch zwei Arten von CSSNumericValue-Objekten dargestellt:

  1. CSSUnitValue: Werte, die einen einzelnen Einheitstyp enthalten (z.B. "42px").
  2. CSSMathValue: Werte, die mehr als einen Wert/eine Einheit enthalten, z. B. mathematische Ausdrücke (z. B. "calc(56em + 10%)").

Einheitenwerte

Einfache numerische Werte ("50%") werden durch CSSUnitValue-Objekte dargestellt. Sie könnten diese Objekte zwar direkt erstellen (new CSSUnitValue(10, 'px')), aber in den meisten Fällen verwenden Sie die CSS.*-Factory-Methoden:

const {value, unit} = CSS.number('10');
// value === 10, unit === 'number'

const {value, unit} = CSS.px(42);
// value === 42, unit === 'px'

const {value, unit} = CSS.vw('100');
// value === 100, unit === 'vw'

const {value, unit} = CSS.percent('10');
// value === 10, unit === 'percent'

const {value, unit} = CSS.deg(45);
// value === 45, unit === 'deg'

const {value, unit} = CSS.ms(300);
// value === 300, unit === 'ms'

Eine vollständige Liste der CSS.*-Methoden finden Sie in der Spezifikation.

Mathematische Werte

CSSMathValue-Objekte stellen mathematische Ausdrücke dar und enthalten in der Regel mehr als einen Wert/eine Einheit. Ein gängiges Beispiel ist das Erstellen eines CSS-calc()-Ausdrucks. Es gibt jedoch Methoden für alle CSS-Funktionen: calc(), min(), max().

new CSSMathSum(CSS.vw(100), CSS.px(-10)).toString(); // "calc(100vw + -10px)"

new CSSMathNegate(CSS.px(42)).toString() // "calc(-42px)"

new CSSMathInvert(CSS.s(10)).toString() // "calc(1 / 10s)"

new CSSMathProduct(CSS.deg(90), CSS.number(Math.PI/180)).toString();
// "calc(90deg * 0.0174533)"

new CSSMathMin(CSS.percent(80), CSS.px(12)).toString(); // "min(80%, 12px)"

new CSSMathMax(CSS.percent(80), CSS.px(12)).toString(); // "max(80%, 12px)"

Verschachtelte Ausdrücke

Die Verwendung der mathematischen Funktionen zum Erstellen komplexerer Werte ist etwas verwirrend. Hier sind einige Beispiele für den Einstieg. Ich habe zusätzliche Einzüge hinzugefügt, um die Lesbarkeit zu verbessern.

calc(1px - 2 * 3em) würde so erstellt:

new CSSMathSum(
  CSS.px(1),
  new CSSMathNegate(
    new CSSMathProduct(2, CSS.em(3))
  )
);

calc(1px + 2px + 3px) würde so erstellt:

new CSSMathSum(CSS.px(1), CSS.px(2), CSS.px(3));

calc(calc(1px + 2px) + 3px) würde so erstellt:

new CSSMathSum(
  new CSSMathSum(CSS.px(1), CSS.px(2)),
  CSS.px(3)
);

Arithmetische Operationen

Eine der nützlichsten Funktionen des CSS Typed OM ist, dass Sie mathematische Operationen für CSSUnitValue-Objekte ausführen können.

Grundlegende Vorgänge

Es werden grundlegende Vorgänge (add/sub/mul/div/min/max) unterstützt:

CSS.deg(45).mul(2) // {value: 90, unit: "deg"}

CSS.percent(50).max(CSS.vw(50)).toString() // "max(50%, 50vw)"

// Can Pass CSSUnitValue:
CSS.px(1).add(CSS.px(2)) // {value: 3, unit: "px"}

// multiple values:
CSS.s(1).sub(CSS.ms(200), CSS.ms(300)).toString() // "calc(1s + -200ms + -300ms)"

// or pass a `CSSMathSum`:
const sum = new CSSMathSum(CSS.percent(100), CSS.px(20)));
CSS.vw(100).add(sum).toString() // "calc(100vw + (100% + 20px))"

Conversion

Absolute Längeneinheiten können in andere Längeneinheiten umgerechnet werden:

// Convert px to other absolute/physical lengths.
el.attributeStyleMap.set('width', '500px');
const width = el.attributeStyleMap.get('width');
width.to('mm'); // CSSUnitValue {value: 132.29166666666669, unit: "mm"}
width.to('cm'); // CSSUnitValue {value: 13.229166666666668, unit: "cm"}
width.to('in'); // CSSUnitValue {value: 5.208333333333333, unit: "in"}

CSS.deg(200).to('rad').value // 3.49066...
CSS.s(2).to('ms').value // 2000

Equality

const width = CSS.px(200);
CSS.px(200).equals(width) // true

const rads = CSS.deg(180).to('rad');
CSS.deg(180).equals(rads.to('deg')) // true

CSS-Transformationswerte

CSS-Transformationen werden mit CSSTransformValue erstellt und es wird ein Array von Transformationswerten übergeben (z.B. CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Angenommen, Sie möchten diesen CSS-Code neu erstellen:

transform: rotateZ(45deg) scale(0.5) translate3d(10px,10px,10px);

Übersetzt in Typed OM:

const transform =  new CSSTransformValue([
  new CSSRotate(CSS.deg(45)),
  new CSSScale(CSS.number(0.5), CSS.number(0.5)),
  new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10))
]);

Neben der Ausführlichkeit (lolz!) CSSTransformValue bietet einige coole Funktionen. Sie hat ein boolesches Attribut zur Unterscheidung von 2D- und 3D-Transformationen und eine .toMatrix()-Methode, um die DOMMatrix-Darstellung einer Transformation zurückzugeben:

new CSSTranslate(CSS.px(10), CSS.px(10)).is2D // true
new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10)).is2D // false
new CSSTranslate(CSS.px(10), CSS.px(10)).toMatrix() // DOMMatrix

Beispiel: Animieren eines Würfels

Sehen wir uns ein praktisches Beispiel für die Verwendung von Transformationen an. Wir verwenden JavaScript und CSS-Transformationen, um einen Würfel zu animieren.

const rotate = new CSSRotate(0, 0, 1, CSS.deg(0));
const transform = new CSSTransformValue([rotate]);

const box = document.querySelector('#box');
box.attributeStyleMap.set('transform', transform);

(function draw() {
  requestAnimationFrame(draw);
  transform[0].angle.value += 5; // Update the transform's angle.
  // rotate.angle.value += 5; // Or, update the CSSRotate object directly.
  box.attributeStyleMap.set('transform', transform); // commit it.
})();

Beachten Sie:

  1. Numerische Werte bedeuten, dass wir den Winkel direkt mithilfe von Mathematik erhöhen können.
  2. Anstatt das DOM zu bearbeiten oder in jedem Frame einen Wert zurückzulesen (z.B. kein box.style.transform=`rotate(0,0,1,${newAngle}deg)`), wird die Animation durch Aktualisieren des zugrunde liegenden Datenobjekts CSSTransformValue gesteuert, was die Leistung verbessert.

Demo

Unten sehen Sie einen roten Würfel, wenn Ihr Browser Typed OM unterstützt. Der Würfel beginnt sich zu drehen, wenn Sie den Mauszeiger darauf bewegen. Die Animation wird durch CSS Typed OM ermöglicht. 🤘

Werte von benutzerdefinierten CSS-Properties

CSS-var() werden im Typed OM zu einem CSSVariableReferenceValue-Objekt. Ihre Werte werden in CSSUnparsedValue geparst, da sie einen beliebigen Typ annehmen können (z. B. „px“, „%“, „em“, „rgba()“).

const foo = new CSSVariableReferenceValue('--foo');
// foo.variable === '--foo'

// Fallback values:
const padding = new CSSVariableReferenceValue(
    '--default-padding', new CSSUnparsedValue(['8px']));
// padding.variable === '--default-padding'
// padding.fallback instanceof CSSUnparsedValue === true
// padding.fallback[0] === '8px'

Wenn Sie den Wert einer benutzerdefinierten Property abrufen möchten, müssen Sie Folgendes tun:

<style>
  body {
    --foo: 10px;
  }
</style>
<script>
  const styles = document.querySelector('style');
  const foo = styles.sheet.cssRules[0].styleMap.get('--foo').trim();
  console.log(CSSNumericValue.parse(foo).value); // 10
</script>

Positionswerte

CSS-Eigenschaften, die eine durch Leerzeichen getrennte x/y-Position wie object-position verwenden, werden durch CSSPositionValue-Objekte dargestellt.

const position = new CSSPositionValue(CSS.px(5), CSS.px(10));
el.attributeStyleMap.set('object-position', position);

console.log(position.x.value, position.y.value);
// → 5, 10

Werte parsen

Das Typed OM führt Parsing-Methoden in die Webplattform ein. Das bedeutet, dass Sie CSS-Werte endlich programmatisch parsen,bevor Sie sie verwenden. Diese neue Funktion kann sehr hilfreich sein, um Fehler und fehlerhaftes CSS frühzeitig zu erkennen.

Vollständigen Stil parsen:

const css = CSSStyleValue.parse(
    'transform', 'translate3d(10px,10px,0) scale(0.5)');
// → css instanceof CSSTransformValue === true
// → css.toString() === 'translate3d(10px, 10px, 0) scale(0.5)'

Werte in CSSUnitValue parsen:

CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}

// But it's easier to use the factory functions:
CSS.px(42.0) // '42px'

Fehlerbehandlung

Beispiel: Prüfen Sie, ob der CSS-Parser mit diesem transform-Wert zufrieden ist:

try {
  const css = CSSStyleValue.parse('transform', 'translate4d(bogus value)');
  // use css
} catch (err) {
  console.err(err);
}

Fazit

Es ist schön, dass es endlich ein aktualisiertes Objektmodell für CSS gibt. Die Arbeit mit Strings hat sich für mich nie richtig angefühlt. Die CSS Typed OM API ist etwas ausführlich, aber hoffentlich führt sie zu weniger Fehlern und leistungsfähigerem Code.