Utilizzare il nuovo modello a oggetti di tipo CSS

Eric Bidelman

TL;DR

Il CSS ora dispone di un'API basata su oggetti appropriata 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 sottili insetti sono finiti.

Introduzione

CSSOM precedente

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

// 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 OM di tipo CSS

Il nuovo modello a oggetti di tipo CSS (Typed Object Model), parte dell'impegno di Houdini, espande questa visione del mondo aggiungendo tipi, metodi e un modello a oggetti corretto ai valori CSS. Al posto delle stringhe, i valori vengono esposti come oggetti JavaScript per facilitare la manipolazione efficace (e sensibile) di CSS.

Anziché utilizzare element.style, accederai agli stili tramite una nuova proprietà .attributeStyleMap per gli elementi e una proprietà .styleMap per le regole del foglio 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é i StylePropertyMap sono oggetti simili a quelli di una mappa, supportano tutti i sospetti presenti (get/set/keys/values/entries), il che li rende flessibili per lavorare con:

// 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 viene restituito un numero quando la proprietà viene letta in un secondo momento.

Vantaggi

Quindi, quali problemi sta cercando di risolvere l'OM di tipo CSS? Osservando gli esempi precedenti (e nel resto di questo articolo), potresti sostenere che l'OM di tipo CSS è molto più dettagliato del precedente modello a oggetti. Sono d'accordo.

Prima di cancellare l'OM Type, considera alcune delle sue funzionalità principali:

  • Meno bug, ad esempio i valori numerici vengono sempre restituiti come numeri, non 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 matematici di base.

  • Clamping e arrotondamento del valore. I valori dell'OM rotondo e/o fissandoli sono stati digitati in modo che rientrino negli intervalli accettabili per una proprietà.

  • Miglior rendimento. Il browser deve svolgere meno attività di serializzazione e deserializzazione dei valori stringa. Ora il motore utilizza una comprensione simile dei valori CSS per JS e C++. Tab Akins ha mostrato alcuni primi benchmark di rendimento che mettono circa il 30% più veloce dell'OM digitato in operazioni/sec rispetto all'utilizzo del vecchio CSSOM e delle vecchie stringhe. Ciò può essere significativo per le animazioni CSS rapide che utilizzano requestionAnimationFrame(). crbug.com/808933 monitora le prestazioni aggiuntive in Blink.

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

  • "Devo utilizzare stringhe o nomi CSS con le maiuscole in cammello?" Non dovrai più chiederti se i nomi contengono lettere maiuscole o minuscole (ad es. el.style.backgroundColor o el.style['background-color']). I nomi delle proprietà CSS nell'OM digitato sono sempre stringhe corrispondenti a ciò che scrivi effettivamente in CSS :)

Supporto del browser e rilevamento delle funzionalità

Il comando OM digitato è stato indirizzato a Chrome 66 e verrà implementato in Firefox. Edge ha mostrato indicazioni di supporto, ma deve ancora aggiungerlo alla dashboard della piattaforma.

Per il rilevamento delle funzionalità, puoi controllare se è definito uno dei fattori numerici CSS.*:

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

Nozioni di base sulle API

Accesso agli stili

I valori sono separati dalle unità nell'OM di tipo CSS. Una volta ottenuto uno stile, viene restituito un valore CSSUnitValue contenente value e 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 elaborati

Gli stili calcolati sono stati spostati da un'API nel giorno window a un nuovo metodo il giorno 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

Arrotondamento / clampaggio dei valori

Una delle caratteristiche interessanti del nuovo modello a oggetti è il blocco 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]. L'OM digitato blocca il valore su 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.

Allo stesso modo, l'impostazione di z-index:15.4 viene arrotondato a 15 in modo che il valore rimanga 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 OM digitato:

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

Valori unitari

I valori numerici semplici ("50%") sono rappresentati da oggetti CSSUnitValue. Anche se potresti creare questi oggetti direttamente (new CSSUnitValue(10, 'px')), nella maggior parte dei casi 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 comune è la creazione di un'espressione calc() CSS, 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 crea un po' di confusione. Di seguito sono riportati alcuni esempi per iniziare. Ho aggiunto un ulteriore rientro per facilitare la lettura.

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

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

calc(1px + 2px + 3px) verrebbe creato come:

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

calc(calc(1px + 2px) + 3px) verrebbe creato come:

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

Operazioni aritmetiche

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

Operazioni di base

Le operazioni di base (add/sub/mul/div/min/max) sono supportate:

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 della trasformazione CSS

Le trasformazioni CSS vengono create con un valore CSSTransformValue e passano un array di valori di trasformazione (ad es. CSSRotate, CSScale, CSSSkew, CSSSkewX,CSSSkewY). Ad esempio, supponi 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 alle Preferenze di lettura, CSSTransformValue ha delle 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 le trasformazioni JavaScript e 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. Con i valori numerici possiamo incrementare l'angolo direttamente con il calcolo matematico.
  2. Anziché modificare il DOM o leggere un valore su ogni frame (ad esempio, nessun box.style.transform=`rotate(0,0,1,${newAngle}deg)`), l'animazione si basa sull'aggiornamento dell'oggetto dati CSSTransformValue sottostante, migliorando le prestazioni.

Demo

Se il browser supporta la digitazione OM, sotto verrà visualizzato un cubo rosso. Il cubo inizia a ruotare quando ci passi il mouse sopra. L'animazione è gestita da CSS Typed OM! 🤘

Valori delle proprietà personalizzate CSS

CSS var() diventa un oggetto CSSVariableReferenceValue nell'OM digitato. I valori vengono analizzati in CSSUnparsedValue perché è possibile utilizzare 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 hanno una posizione x/y separata da spazi, come 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 Typed 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à è un potenziale risparmio di vita per l'individuazione dei primi bug e dei CSS con formato non valido.

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: controlla se l'analizzatore sintattico CSS sarà soddisfatto di questo valore transform:

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

Conclusione

È bello finalmente disporre di un modello a oggetti aggiornato per CSS. Lavorare con gli archi non mi è mai sembrato giusto. L'API OM di tipo CSS è un po' dettagliata, ma si spera che comporti meno bug e un codice più performante in futuro.