Werken met het nieuwe CSS Typed Object Model

Kortom

CSS beschikt nu over een echte objectgebaseerde API voor het werken met waarden in JavaScript.

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

De tijd van het aaneenschakelen van strings en subtiele bugs is voorbij!

Invoering

Oude CSSOM

CSS heeft al jaren een objectmodel (CSSOM). Sterker nog, elke keer dat je .style in JavaScript leest/instelt, gebruik je het:

// 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;

Nieuwe CSS getypte OM

Het nieuwe CSS Typed Object Model (Typed OM), onderdeel van Houdini's project, breidt dit wereldbeeld uit door typen, methoden en een eigen objectmodel toe te voegen aan CSS-waarden. In plaats van strings worden waarden weergegeven als JavaScript-objecten om performante (en zinvolle) manipulatie van CSS te vergemakkelijken.

In plaats van element.style te gebruiken, krijgt u toegang tot stijlen via een nieuwe eigenschap .attributeStyleMap voor elementen en een eigenschap .styleMap voor stylesheetregels. Beide retourneren een StylePropertyMap object.

// 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');

Omdat StylePropertyMap objecten zijn die lijken op Map, ondersteunen ze alle gebruikelijke functies (get/set/keys/values/entries), waardoor er flexibel mee gewerkt kan worden:

// 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.

Houd er rekening mee dat in het tweede voorbeeld opacity is ingesteld op string ( '0.3' ), maar dat er een getal terugkomt wanneer de eigenschap later wordt uitgelezen.

Voordelen

Dus welke problemen probeert CSS Typed OM op te lossen? Kijkend naar de voorbeelden hierboven (en de rest van dit artikel), zou je kunnen stellen dat CSS Typed OM veel uitgebreider is dan het oude objectmodel. Daar ben ik het mee eens!

Voordat u Typed OM afschrijft, moet u rekening houden met enkele belangrijke functies die het met zich meebrengt:

  • Minder bugs . Bijvoorbeeld: numerieke waarden worden altijd geretourneerd als getallen, en niet als strings.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Rekenkundige bewerkingen en eenhedenconversie . Converteer tussen absolute lengte-eenheden (bijv. px -> cm ) en voer basisberekeningen uit .

  • Waardeafronding en -klemming . Getypte OM rondt en/of klemt waarden af ​​zodat ze binnen de acceptabele bereiken voor een eigenschap vallen.

  • Betere prestaties . De browser hoeft minder werk te doen met het serialiseren en deserialiseren van stringwaarden. Nu gebruikt de engine een vergelijkbare interpretatie van CSS-waarden in JS en C++. Tab Akins heeft een aantal vroege prestatiebenchmarks laten zien die Typed OM op ~30% snellere bewerkingen per seconde zetten in vergelijking met de oude CSSOM en strings. Dit kan aanzienlijk zijn voor snelle CSS-animaties met requestionAnimationFrame() . crbug.com/808933 houdt extra prestatieverbeteringen in Blink bij.

  • Foutbehandeling . Nieuwe parseermethoden brengen foutbehandeling in de wereld van CSS.

  • "Moet ik CSS-namen of strings in camel-case gebruiken?" Je hoeft niet meer te gissen of namen camel-case of strings zijn (bijv. el.style.backgroundColor versus el.style['background-color'] ). CSS-eigenschapsnamen in Typed OM zijn altijd strings, wat overeenkomt met wat je daadwerkelijk in CSS schrijft :)

Browserondersteuning en functiedetectie

Typed OM is beschikbaar in Chrome 66 en wordt geïmplementeerd in Firefox. Edge heeft tekenen van ondersteuning laten zien, maar heeft het nog niet toegevoegd aan hun platformdashboard .

Voor het detecteren van kenmerken kunt u controleren of een van de CSS.* numerieke fabrieken is gedefinieerd:

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

API-basisprincipes

Toegang tot stijlen

Waarden zijn gescheiden van eenheden in CSS Typed OM. Het ophalen van een stijl retourneert een CSSUnitValue met een value en unit :

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

Berekende stijlen

Berekende stijlen zijn verplaatst van een API in window naar een nieuwe methode in HTMLElement , computedStyleMap() :

Oude CSSOM

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

Nieuwe getypte OM

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

Waardevastlegging / afronding

Een van de leuke functies van het nieuwe objectmodel is het automatisch vastklemmen en/of afronden van berekende stijlwaarden . Stel bijvoorbeeld dat u opacity probeert in te stellen op een waarde buiten het acceptabele bereik, [0, 1]. Getypte OM klemt de waarde vast op 1 bij het berekenen van de stijl:

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

Op dezelfde manier wordt bij het instellen van z-index:15.4 afgerond naar 15 waardoor de waarde een geheel getal blijft.

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.

CSS-numerieke waarden

Getallen worden weergegeven door twee typen CSSNumericValue objecten in Typed OM:

  1. CSSUnitValue - waarden die één eenheidstype bevatten (bijv. "42px" ).
  2. CSSMathValue - waarden die meer dan één waarde/eenheid bevatten, zoals een wiskundige uitdrukking (bijv. "calc(56em + 10%)" ).

Eenheidswaarden

Eenvoudige numerieke waarden ( "50%" ) worden weergegeven door CSSUnitValue objecten. Hoewel u deze objecten rechtstreeks kunt aanmaken ( new CSSUnitValue(10, 'px') ), zult u meestal de CSS.* fabrieksmethoden gebruiken:

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'

Zie de specificatie voor de volledige lijst met CSS.* methoden.

Wiskundige waarden

CSSMathValue objecten vertegenwoordigen wiskundige expressies en bevatten doorgaans meer dan één waarde/eenheid. Het meest voorkomende voorbeeld is het aanmaken van een CSS calc() expressie, maar er zijn methoden voor alle CSS-functies: calc() , min() en 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)"

Geneste expressies

Het gebruik van wiskundige functies om complexere waarden te creëren kan wat verwarrend zijn. Hieronder staan ​​een paar voorbeelden om je op weg te helpen. Ik heb extra inspringing toegevoegd om ze gemakkelijker leesbaar te maken.

calc(1px - 2 * 3em) zou als volgt worden geconstrueerd:

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

calc(1px + 2px + 3px) zou als volgt worden geconstrueerd:

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

calc(calc(1px + 2px) + 3px) zou als volgt worden geconstrueerd:

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

Rekenkundige bewerkingen

Een van de handigste functies van The CSS Typed OM is dat u wiskundige bewerkingen kunt uitvoeren op CSSUnitValue objecten.

Basisbewerkingen

Basisbewerkingen ( add / sub / mul / div / min / max ) worden ondersteund:

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))"

Conversie

Absolute lengte-eenheden kunnen worden omgezet naar andere lengte-eenheden:

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

Gelijkwaardigheid

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-transformatiewaarden

CSS-transformaties worden gemaakt met een CSSTransformValue en geven een reeks transformatiewaarden door (bijv. CSSRotate , CSScale , CSSSkew , CSSSkewX , CSSSkewY ). Stel dat u bijvoorbeeld deze CSS opnieuw wilt maken:

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

Vertaald naar getypte 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))
]);

Naast de uitgebreide uitleg (lolz!), heeft CSSTransformValue een aantal coole functies. Het heeft een booleaanse eigenschap om onderscheid te maken tussen 2D- en 3D-transformaties en een .toMatrix() methode om de DOMMatrix -representatie van een transformatie te retourneren:

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

Voorbeeld: een kubus animeren

Laten we eens kijken naar een praktisch voorbeeld van het gebruik van transformaties. We gebruiken JavaScript- en CSS-transformaties om een ​​kubus te animeren.

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.
})();

Let op:

  1. Numerieke waarden betekenen dat we de hoek direct kunnen vergroten met behulp van wiskunde!
  2. In plaats van de DOM aan te passen of bij elk frame een waarde terug te lezen (bijvoorbeeld geen box.style.transform=`rotate(0,0,1,${newAngle}deg)` ), wordt de animatie aangestuurd door het onderliggende CSSTransformValue gegevensobject bij te werken, waardoor de prestaties worden verbeterd .

Demonstratie

Hieronder zie je een rode kubus als je browser Typed OM ondersteunt. De kubus begint te draaien wanneer je er met de muis overheen beweegt. De animatie wordt aangestuurd door CSS Typed OM! 🤘

Waarden voor aangepaste CSS-eigenschappen

CSS var() wordt een CSSVariableReferenceValue object in de getypeerde OM. Hun waarden worden geparseerd naar CSSUnparsedValue omdat ze elk type kunnen aannemen (px, %, em, rgba(), enz.).

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'

Als u de waarde van een op maat gemaakt onroerend goed wilt weten, moet u het volgende doen:

<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>

Positiewaarden

CSS-eigenschappen die een door spaties gescheiden x/y-positie innemen, zoals object-position worden weergegeven door CSSPositionValue objecten.

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

Waarden parsen

De Typed OM introduceert parsing-methoden op het webplatform! Dit betekent dat je CSS-waarden eindelijk programmatisch kunt parsen voordat je ze daadwerkelijk gebruikt ! Deze nieuwe mogelijkheid is een potentiële levensredder om bugs en misvormde CSS in een vroeg stadium op te sporen.

Een volledige stijl parseren:

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)'

Waarden parseren in CSSUnitValue :

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

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

Foutafhandeling

Voorbeeld : controleer of de CSS-parser tevreden is met deze transform :

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

Conclusie

Het is fijn om eindelijk een bijgewerkt objectmodel voor CSS te hebben. Werken met strings voelde voor mij nooit goed. De CSS Typed OM API is wat omslachtig, maar hopelijk resulteert dit in minder bugs en beter presterende code op de lange termijn.