Mit dem neuen CSS Typed-Objektmodell arbeiten

Eric Bidelman

Kurzfassung

CSS verfügt jetzt über eine ordnungsgemäße 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 Strings und kleine Fehler verkettet werden, sind vorbei!

Einführung

Altes CSSOM

Für Preisvergleichsportale gibt es seit vielen Jahren ein Objektmodell (CSSOM). Jedes Mal, wenn Sie .style in JavaScript lesen/festlegen, verwenden Sie den Code:

// 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-Eingabe-OM

Das neue CSS Typed Object Model (Typed OM) ist Teil der Houdini-Initiative und erweitert diese Weltansicht um Typen, Methoden und ein geeignetes Objektmodell zu CSS-Werten. Anstelle von Strings werden Werte als JavaScript-Objekte zur Verfügung gestellt, um eine leistungsstarke (und sinnvolle) Manipulation von CSS zu ermöglichen.

Anstatt element.style zu verwenden, greifen Sie auf Stile über eine neue .attributeStyleMap-Eigenschaft für Elemente und über eine .styleMap-Eigenschaft für Stylesheet-Regeln 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 kartenähnliche Objekte sind, unterstützen sie alle üblichen Verdächtigen (get/set/keys/values/entries), sodass sie flexibel verwendet werden können:

// 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 den String ('0.3') festgelegt ist, aber eine Zahl zurückgegeben wird, wenn das Attribut später gelesen wird.

Vorteile

Welche Probleme versucht das CSS Typed OM zu lösen? Wenn Sie sich die Beispiele oben und die übrigen Bestandteile dieses Artikels ansehen, könnten Sie der Meinung sein, dass das CSS Typed OM viel ausführlicher ist als das alte Objektmodell. Da stimme ich zu.

Bevor Sie Typed OM ausschreiben, sollten Sie einige der wichtigsten Funktionen berücksichtigen:

  • Weniger Fehler: Numerische Werte werden z. B. 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. Wandeln Sie zwischen absoluten Längeneinheiten (z.B. px -> cm) um und führen Sie einfache Berechnungen durch.

  • Bindung und Rundung von Werten: Die eingegebenen OM-Werte runden und/oder klemmen, sodass sie innerhalb der zulässigen Bereiche für eine Eigenschaft liegen.

  • Bessere Leistung: Der Browser muss weniger Arbeit beim Serialisieren und Deserialisieren von Stringwerten erledigen. Jetzt verwendet die Engine ein ähnliches Verständnis von CSS-Werten in JS und C++. Tab Akins hat einige Early Performance-Benchmarks gezeigt, durch die die typisierte OM im Vergleich zur Verwendung des alten CSSOM und Strings etwa 30% schneller bei Vorgängen pro Sekunde liegt. Dies kann bei schnellen CSS-Animationen mit requestionAnimationFrame() von Bedeutung sein. Unter crbug.com/808933 können Sie zusätzliche Leistungsarbeiten in Blink verfolgen.

  • Fehlerbehandlung: Neue Parsing-Methoden bieten eine Fehlerbehandlung in der Welt von CSS.

  • „Sollte ich CSS-Namen oder -Strings in der Camel-Case-Schreibweise verwenden?“ Sie müssen nicht mehr raten, ob Namen in Camel-Case-Schreibweise vorkommen, oder Strings (z. B. el.style.backgroundColor oder el.style['background-color']). CSS-Eigenschaftsnamen im Typed OM sind immer Strings, die mit dem übereinstimmen, was Sie tatsächlich in CSS schreiben.

Browserunterstützung und Funktionserkennung

Typed OM wurde in Chrome 66 eingeführt und wird jetzt in Firefox implementiert. Edge hat Anzeichen für Unterstützung angezeigt, muss diese jedoch dem Plattform-Dashboard noch hinzufügen.

Für die Featureerkennung können Sie prüfen, ob eine der numerischen CSS.*-Faktoren definiert ist:

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

API-Grundlagen

Zugreifen auf Stile

Die Werte sind getrennt von den Einheiten in CSS Typed OM. Wenn Sie einen Stil abrufen, wird ein CSSUnitValue zurückgegeben, das value und unit enthält:

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 in window in eine neue Methode in HTMLElement, computedStyleMap(), verschoben:

Altes CSSOM

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

Neu eingegebenes OM

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

Wertklemmen / Abrunden

Eines der praktischen Merkmale des neuen Objektmodells ist das automatische Aufteilen und/oder Runden von berechneten Stilwerten. Beispiel: Sie versuchen, opacity auf einen Wert außerhalb des zulässigen Bereichs festzulegen [0, 1]. Das eingegebene OM setzt den Wert bei der Berechnung des Stils auf 1:

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 auf 15 gerundet, sodass der Wert 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 in Typed OM durch zwei Typen von CSSNumericValue-Objekten dargestellt:

  1. CSSUnitValue: Werte, die einen einzelnen Einheitentyp enthalten (z.B. "42px").
  2. CSSMathValue: Werte, die mehr als einen Wert/eine Einheit enthalten, z. B. ein mathematischer Ausdruck (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')), verwenden aber in den meisten Fällen die Factory-Methoden für CSS.*:

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'

Die 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. Das übliche 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 mathematischer Funktionen zum Erstellen komplexerer Werte kann etwas verwirrend sein. Im Folgenden finden Sie einige Beispiele für den Einstieg. habe ich eine zusätzliche Einrückung hinzugefügt, um sie besser lesbar zu machen.

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

Eines der nützlichsten Funktionen des CSS Typed OM ist, dass Sie mathematische Operationen an CSSUnitValue-Objekten ausführen können.

Grundlegende Vorgänge

Grundlegende Vorgänge (add/sub/mul/div/min/max) werden 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 umgewandelt 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

Entsprechung

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 einer CSSTransformValue erstellt und übergeben ein Array von Transformationswerten (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 folgendes geschriebenes 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))
]);

Abgesehen von der Ausführlichkeit (lolz!) CSSTransformValue hat tolle Funktionen. Sie hat eine boolesche Eigenschaft, um 2D- und 3D-Transformationen zu unterscheiden, 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: Würfel animieren

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 mit Mathematik erhöhen können.
  2. Anstatt das DOM zu berühren oder einen Wert für jeden Frame zurückzulesen (z.B. ohne box.style.transform=`rotate(0,0,1,${newAngle}deg)`), wird die Animation durch das Aktualisieren des zugrunde liegenden CSSTransformValue-Datenobjekts gesteuert, um die Leistung zu verbessern.

Demo

Wenn Ihr Browser Typed OM unterstützt, sehen Sie unten einen roten Würfel. Der Kubus beginnt sich zu drehen, wenn Sie die Maus darüber bewegen. Die Animation wird von CSS Typed OM! 🤘

Werte benutzerdefinierter CSS-Eigenschaften

CSS-var() wird zu einem CSSVariableReferenceValue-Objekt im typisierten OM. Ihre Werte werden in CSSUnparsedValue geparst, da sie jeden Typ annehmen können (px, %, em, rgba() usw.).

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 Eigenschaft abrufen möchten, sind einige Schritte erforderlich:

<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-Attribute, die eine durch Leerzeichen getrennte x/y-Position wie object-position einnehmen, 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 auf der Webplattform ein. Das bedeutet, dass Sie endlich CSS-Werte programmatisch parsen können, bevor Sie versuchen, sie zu verwenden. Mit dieser neuen Funktion lassen sich frühe Programmfehler und fehlerhaftes CSS leichter 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

Wir freuen uns, dass wir nun ein aktualisiertes Objektmodell für CSS haben. Ich fand die Arbeit mit Saiten nie gut. Die CSS Typed OM API ist etwas ausführlicher, führt aber hoffentlich zu weniger Programmfehlern und einem leistungsfähigeren Code.