Utilizzare il nuovo modello a oggetti di tipo CSS

TL;DR

Ora CSS dispone di un'API basata su oggetti per lavorare con i valori in JavaScript.

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

I giorni della concatenazione di stringhe e dei bug sottili sono finiti.

Introduzione

CSSOM precedente

Il CSS ha un modello a oggetti (CSSOM) da molti anni. Infatti, lo utilizzi ogni volta che leggi/imposti .style in JavaScript:

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

Nuovo CSS Typed OM

Il nuovo CSS Typed Object Model (Typed OM), parte del progetto Houdini, espande questa visione del mondo aggiungendo tipi, metodi e un modello di oggetti appropriato ai valori CSS. Anziché stringhe, i valori vengono esposti come oggetti JavaScript per facilitare la manipolazione efficiente (e sensata) del CSS.

Anziché utilizzare element.style, accederai agli stili tramite una nuova proprietà .attributeStyleMap per gli elementi e una proprietà .styleMap per le regole dei fogli di stile. Entrambi restituiscono un oggetto StylePropertyMap.

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

Poiché StylePropertyMap sono oggetti simili a mappe, supportano tutti i metodi più comuni (get/set/keys/values/entries), il che li rende flessibili da usare:

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

Tieni presente che nel secondo esempio opacity è impostato sulla stringa ('0.3'), ma quando la proprietà viene letta di nuovo in un secondo momento, viene restituito un numero.

Vantaggi

Quali problemi cerca di risolvere CSS Typed OM? Se esamini gli esempi riportati sopra (e nel resto di questo articolo), potresti sostenere che CSS Typed OM è molto più dettagliato del vecchio modello a oggetti. Sono d'accordo.

Prima di escludere Typed OM, considera alcune delle funzionalità chiave che offre:

  • Meno bug: ad esempio, i valori numerici vengono sempre restituiti come numeri, non come stringhe.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Operazioni aritmetiche e conversione di unità. Converti tra unità di lunghezza assoluta (ad es. px -> cm) ed esegui calcoli di base.

  • Blocco e arrotondamento dei valori. L'OM digitato arrotonda e/o limita i valori in modo che rientrino negli intervalli accettabili per una proprietà.

  • Miglior rendimento. Il browser deve eseguire meno operazioni di serializzazione e deserializzazione dei valori stringa. Ora il motore utilizza una comprensione simile dei valori CSS in JS e C++. Tab Akins ha mostrato alcuni benchmark delle prestazioni iniziali che indicano che Typed OM è circa il 30% più veloce in operazioni/sec rispetto all'utilizzo del vecchio CSSOM e delle stringhe. Questo può essere significativo per le animazioni CSS rapide che utilizzano requestionAnimationFrame(). crbug.com/808933 monitora il lavoro aggiuntivo sulle prestazioni in Blink.

  • Gestione degli errori. I nuovi metodi di analisi introducono la gestione degli errori nel mondo dei CSS.

  • "Devo utilizzare nomi o stringhe CSS in camel case?" Non è più necessario indovinare se i nomi sono in formato camel case o stringhe (ad es. el.style.backgroundColor vs el.style['background-color']). I nomi delle proprietà CSS in Typed OM sono sempre stringhe, corrispondenti a ciò che scrivi effettivamente in CSS :)

Supporto del browser e rilevamento delle funzionalità

L'OM digitato è stato introdotto in Chrome 66 e verrà implementato in Firefox. Edge ha mostrato segni di supporto, ma deve ancora aggiungerlo alla dashboard della piattaforma.

Per il rilevamento delle funzionalità, puoi verificare se è definita una delle fabbriche numeriche CSS.*:

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

Nozioni di base sulle API

Accedere agli stili

I valori sono separati dalle unità in CSS Typed OM. L'ottenimento di uno stile restituisce un CSSUnitValue contenente un value e un 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

Stili calcolati

Gli stili calcolati sono stati spostati da un'API su window a un nuovo metodo su HTMLElement, computedStyleMap():

CSSOM precedente

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

Nuovo OM digitato

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

Limitazione / arrotondamento dei valori

Una delle funzionalità interessanti del nuovo modello a oggetti è il bloccaggio automatico e/o l'arrotondamento dei valori di stile calcolati. Ad esempio, supponiamo che tu provi a impostare opacity su un valore al di fuori dell'intervallo accettabile [0, 1]. Il tipo OM blocca il valore a 1 durante il calcolo dello stile:

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

Analogamente, l'impostazione z-index:15.4 viene arrotondata a 15, quindi il valore rimane un numero intero.

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.

Valori numerici CSS

I numeri sono rappresentati da due tipi di oggetti CSSNumericValue in Typed OM:

  1. CSSUnitValue: valori che contengono un solo tipo di unità (ad es. "42px").
  2. CSSMathValue: valori che contengono più di un valore/unità, ad esempio un'espressione matematica (ad es. "calc(56em + 10%)").

Valori unità

I valori numerici semplici ("50%") sono rappresentati da oggetti CSSUnitValue. Anche se potresti creare questi oggetti direttamente (new CSSUnitValue(10, 'px')), la maggior parte delle volte utilizzerai i metodi di fabbrica 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'

Consulta le specifiche per l'elenco completo dei metodi CSS.*.

Valori matematici

Gli oggetti CSSMathValue rappresentano espressioni matematiche e in genere contengono più di un valore/unità. L'esempio più comune è la creazione di un'espressione CSS calc(), ma esistono metodi per tutte le funzioni CSS: 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)"

Espressioni nidificate

L'utilizzo delle funzioni matematiche per creare valori più complessi diventa un po' complicato. Di seguito sono riportati alcuni esempi per iniziare. Ho aggiunto un rientro extra per renderli più facili da leggere.

calc(1px - 2 * 3em) verrebbe costruito come segue:

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

calc(1px + 2px + 3px) verrebbe costruito come segue:

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

calc(calc(1px + 2px) + 3px) verrebbe costruito come segue:

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

Operazioni aritmetiche

Una delle funzionalità più utili di CSS Typed OM è la possibilità di eseguire operazioni matematiche sugli oggetti CSSUnitValue.

Operazioni di base

Sono supportate le operazioni di base (add/sub/mul/div/min/max):

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

Conversione

Le unità di lunghezza assoluta possono essere convertite in altre unità di lunghezza:

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

Valori di trasformazione CSS

Le trasformazioni CSS vengono create con CSSTransformValue e passando un array di valori di trasformazione (ad es. CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Ad esempio, supponiamo di voler ricreare questo CSS:

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

Tradotto in OM digitato:

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))
]);

Oltre alla sua verbosità (lolz!), CSSTransformValue ha alcune funzionalità interessanti. Ha una proprietà booleana per distinguere le trasformazioni 2D e 3D e un metodo .toMatrix() per restituire la rappresentazione DOMMatrix di una trasformazione:

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

Esempio: animare un cubo

Vediamo un esempio pratico di utilizzo delle trasformazioni. Utilizzeremo JavaScript e le trasformazioni CSS per animare un cubo.

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

Tieni presente che:

  1. I valori numerici ci consentono di incrementare l'angolo direttamente utilizzando la matematica.
  2. Anziché toccare il DOM o leggere un valore su ogni frame (ad es. no box.style.transform=`rotate(0,0,1,${newAngle}deg)`), l'animazione è guidata dall'aggiornamento dell'oggetto dati CSSTransformValue sottostante, migliorando le prestazioni.

Demo

Di seguito, vedrai un cubo rosso se il browser supporta Typed OM. Il cubo inizia a ruotare quando passi il mouse sopra. L'animazione è basata su CSS Typed OM. 🤘

Valori delle proprietà personalizzate CSS

Il CSS var() diventa un oggetto CSSVariableReferenceValue in Typed OM. I loro valori vengono analizzati in CSSUnparsedValue perché possono assumere qualsiasi tipo (px, %, em, rgba() e così via).

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'

Se vuoi ottenere il valore di una proprietà personalizzata, devi fare un po' di lavoro:

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

Valori di posizione

Le proprietà CSS che accettano una posizione x/y separata da spazi, ad esempio object-position, sono rappresentate da oggetti CSSPositionValue.

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

Analisi dei valori

L'OM digitato introduce metodi di analisi nella piattaforma web. Ciò significa che puoi finalmente analizzare i valori CSS in modo programmatico, prima di provare a utilizzarli. Questa nuova funzionalità può essere un vero e proprio salvavita per rilevare bug e CSS malformati in fase iniziale.

Analizza uno stile completo:

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

Analizza i valori in CSSUnitValue:

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

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

Gestione degli errori

Esempio: verifica se il parser CSS accetta questo valore transform:

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

Conclusione

È bello avere finalmente un modello a oggetti aggiornato per CSS. Lavorare con le stringhe non mi è mai sembrato giusto. L'API CSS Typed OM è un po' prolissa, ma si spera che porti a meno bug e a un codice più performante in futuro.