Utiliser le nouveau modèle d'objet typé CSS

Eric Bidelman

Résumé

CSS dispose désormais d'une API basée sur les objets appropriée pour utiliser des valeurs en JavaScript.

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

Fini le temps où il fallait concaténer des chaînes et bugs subtils !

Introduction

Ancien code CSSOM

Les CSS possèdent un modèle d'objet (CSSOM) depuis de nombreuses années. Chaque fois que vous lisez ou définissez .style en JavaScript, vous l'utilisez:

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

Nouveau OM de type CSS

Le nouveau CSS Typed Object Model (OMD type), une partie de l'initiative Houdini, élargit cette vision du monde en ajoutant des types, des méthodes et un modèle d'objet approprié aux valeurs CSS. Au lieu de chaînes, les valeurs sont exposées en tant qu'objets JavaScript pour faciliter une manipulation performante (et raisonnable) du CSS.

Au lieu d'utiliser element.style, vous accéderez aux styles via une nouvelle propriété .attributeStyleMap pour les éléments et une propriété .styleMap pour les règles de feuille de style. Les deux renvoient un objet 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');

Étant donné que les StylePropertyMap sont des objets de type Map, ils sont compatibles avec tous les suspects habituels (get/set/keys/values/inputs), ce qui les rend flexibles pour leur utilisation:

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

Notez que dans le deuxième exemple, opacity est défini sur "string" ('0.3'), mais un nombre s'affiche lorsque la propriété est lue ultérieurement.

Avantages

Quels problèmes l'OMS de type CSS tente-t-il de résoudre ? En examinant les exemples ci-dessus (et tout au long de cet article), vous pouvez considérer que la commande OM de type CSS est beaucoup plus détaillée que l'ancien modèle d'objet. Je suis d'accord !

Avant d'écrire l'OM typé, tenez compte de certaines des principales caractéristiques qu'il apporte:

  • Moins de bugs. Par exemple, les valeurs numériques sont toujours renvoyées sous forme de nombres et non de chaînes.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Opérations arithmétiques et conversion d'unités : convertissez des unités de longueur absolue (par exemple, px -> cm) et effectuez des calculs de base.

  • Limitation et arrondi de la valeur : Saisissez les arrondis et/ou limites OM afin qu'ils soient compris dans les plages acceptables pour une propriété.

  • Performances améliorées. Le navigateur doit réaliser moins de travail de sérialisation et de désérialisation des valeurs de chaîne. À présent, le moteur utilise une compréhension similaire des valeurs CSS en JavaScript et C++. Tab Akins a présenté quelques benchmarks de performances précoces qui ont permis d'effectuer environ 30% plus rapidement d'opérations par seconde qu'avec l'ancien CSSOM et les anciennes chaînes. Cela peut se révéler utile pour les animations CSS rapides utilisant requestionAnimationFrame(). crbug.com/808933 suit les tâches de performances supplémentaires dans Blink.

  • Traitement des erreurs. De nouvelles méthodes d'analyse permettent de gérer les erreurs dans l'univers CSS.

  • "Dois-je utiliser des chaînes ou des noms CSS en Camel Case ("casse de chameau") ? Plus besoin de deviner si les noms sont en camelcase ou en chaînes (par exemple, el.style.backgroundColor ou el.style['background-color']). Les noms de propriétés CSS dans l'OM typé sont toujours des chaînes, correspondant à ce que vous écrivez réellement en CSS :)

Prise en charge des navigateurs et détection des fonctionnalités

La commande OM a été saisie dans Chrome 66 et est en cours d'implémentation dans Firefox. Edge présente des signes de compatibilité, mais ne l'a pas encore ajouté à son tableau de bord de plate-forme.

Pour la détection de caractéristiques, vous pouvez vérifier si l'une des fabriques numériques CSS.* est définie:

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

Principes de base des API

Accéder aux styles

Les valeurs sont distinctes des unités dans les blocs de type CSS OM. L'obtention d'un style renvoie un CSSUnitValue contenant un value et 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

Styles calculés

Les styles calculés ont été déplacés d'une API sur window vers une nouvelle méthode sur HTMLElement, computedStyleMap():

Ancien code CSSOM

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

Nouvel OM saisi

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

Limitation / Arrondi de la valeur

L'une des fonctionnalités intéressantes du nouveau modèle d'objet est la limitation et/ou l'arrondi automatiques des valeurs de style calculées. Par exemple, supposons que vous essayiez de définir opacity sur une valeur en dehors de la plage acceptable, [0, 1]. Si vous saisissez OM, la valeur est réduite à 1 lors du calcul du style:

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

De même, si vous définissez z-index:15.4 arrondit à 15, la valeur reste un entier.

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.

Valeurs numériques CSS

Les nombres sont représentés par deux types d'objets CSSNumericValue dans OM typé:

  1. CSSUnitValue : valeurs contenant un seul type d'unité (par exemple, "42px").
  2. CSSMathValue : valeurs contenant plusieurs valeurs/unités, comme une expression mathématique (par exemple, "calc(56em + 10%)").

Valeurs unitaires

Les valeurs numériques simples ("50%") sont représentées par des objets CSSUnitValue. Bien que vous puissiez créer ces objets directement (new CSSUnitValue(10, 'px')), vous utiliserez la plupart du temps les méthodes de fabrique 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'

Consultez la spécification pour obtenir la liste complète des méthodes CSS.*.

Valeurs mathématiques

Les objets CSSMathValue représentent des expressions mathématiques et contiennent généralement plusieurs valeurs/unités. L'exemple courant consiste à créer une expression CSS calc(), mais il existe des méthodes pour toutes les fonctions CSS : calc(), min() et 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)"

Expressions imbriquées

L'utilisation des fonctions mathématiques pour créer des valeurs plus complexes peut prêter à confusion. Vous trouverez ci-dessous quelques exemples pour vous aider à démarrer. j'ai ajouté une mise en retrait supplémentaire pour les rendre plus faciles à lire.

calc(1px - 2 * 3em) se présenterait comme suit:

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

calc(1px + 2px + 3px) se présenterait comme suit:

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

calc(calc(1px + 2px) + 3px) se présenterait comme suit:

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

Opérations arithmétiques

L'une des fonctionnalités les plus utiles de l'OM typé CSS vous permet d'effectuer des opérations mathématiques sur des objets CSSUnitValue.

Opérations de base

Les opérations de base (add/sub/mul/div/min/max) sont acceptées:

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

Les unités de longueur absolue peuvent être converties en d'autres longueurs d'unités:

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

Égalité

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

Valeurs de transformation CSS

Les transformations CSS sont créées avec un CSSTransformValue et la transmission d'un tableau de valeurs de transformation (par exemple, CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Par exemple, imaginons que vous souhaitiez recréer ce CSS:

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

Traduit en OM saisi:

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

En plus de la verbosité (lolz !), CSSTransformValue présente des fonctionnalités intéressantes. Elle dispose d'une propriété booléenne permettant de différencier les transformations 2D et 3D, ainsi que d'une méthode .toMatrix() pour renvoyer la représentation DOMMatrix d'une transformation:

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

Exemple: animer un cube

Voyons un exemple pratique d'utilisation de transformations. Nous allons utiliser les transformations JavaScript et CSS pour animer un cube.

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

Remarque :

  1. Les valeurs numériques signifient que nous pouvons incrémenter l'angle directement à l'aide de calculs mathématiques !
  2. Plutôt que de toucher le DOM ou de lire une valeur sur chaque frame (par exemple, pas de box.style.transform=`rotate(0,0,1,${newAngle}deg)`), l'animation est guidée en modifiant l'objet de données CSSTransformValue sous-jacent, ce qui améliore les performances.

Démonstration

Ci-dessous, un cube rouge s'affiche si votre navigateur prend en charge l'OM typé. Le cube commence à pivoter lorsque vous passez la souris dessus. L'animation est fournie par CSS Typed OM. 🤘

Valeurs des propriétés personnalisées CSS

Le CSS var() devient un objet CSSVariableReferenceValue dans l'OM typé. Leurs valeurs sont analysées dans CSSUnparsedValue, car elles peuvent prendre n'importe quel type (px, %, em, rgba(), etc.).

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'

Si vous souhaitez obtenir la valeur d'une propriété personnalisée, vous devez effectuer quelques opérations:

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

Valeurs de position

Les propriétés CSS qui acceptent une position x/y séparée par un espace, telles que object-position, sont représentées par des objets 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

Analyser des valeurs

L'OM typé présente des méthodes d'analyse à la plate-forme Web. Cela signifie que vous pouvez enfin analyser les valeurs CSS de manière programmatique, avant d'essayer de les utiliser. Cette nouvelle fonctionnalité peut vous sauver la vie en identifiant les premiers bugs et les fichiers CSS mal formés.

Analyser un style complet:

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

Analysez les valeurs dans CSSUnitValue:

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

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

Gestion des exceptions

Exemple – Vérifiez si l'analyseur CSS est satisfait de la valeur transform:

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

Conclusion

C'est bien d'avoir enfin un modèle d'objet mis à jour pour CSS. Travailler avec des cordes ne me m'a jamais plu. L'API CSS Typed OM est un peu détaillée, mais elle devrait réduire le nombre de bugs et améliorer les performances de code par la suite.