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

TL;DR

Le CSS dispose désormais d'une API basée sur les objets pour travailler avec les valeurs en JavaScript.

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

L'époque des concaténations de chaînes et des bugs subtils est révolue !

Introduction

Ancien CSSOM

Le CSS dispose d'un modèle d'objet (CSSOM) depuis de nombreuses années. En fait, chaque fois que vous lisez/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 CSS Typed OM

Le nouveau CSS Typed Object Model (Typed OM), qui fait partie du projet Houdini, élargit cette vision du monde en ajoutant des types, des méthodes et un véritable modèle d'objet aux valeurs CSS. Au lieu de chaînes, les valeurs sont exposées en tant qu'objets JavaScript pour faciliter la manipulation performante (et judicieuse) 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/entries), ce qui les rend flexibles à utiliser :

// 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 la chaîne ('0.3'), mais qu'un nombre est renvoyé lorsque la propriété est relue ultérieurement.

Avantages

Quels problèmes le CSS Typed OM tente-t-il de résoudre ? En regardant les exemples ci-dessus (et tout au long de cet article), vous pourriez soutenir que le CSS Typed OM est beaucoup plus verbeux que l'ancien modèle objet. Je suis d'accord !

Avant de renoncer à Typed OM, examinez certaines de ses principales fonctionnalités :

  • 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 des valeurs L'OM typé arrondit et/ou limite les valeurs pour qu'elles se situent dans les plages acceptables pour une propriété.

  • Des performances améliorées : Le navigateur a moins de travail à faire pour sérialiser et désérialiser les valeurs de chaîne. Le moteur utilise désormais une compréhension similaire des valeurs CSS dans JS et C++. Tab Akins a présenté quelques premiers benchmarks de performances qui indiquent que Typed OM est environ 30 % plus rapide en termes d'opérations/s par rapport à l'utilisation de l'ancien CSSOM et des chaînes. Cela peut être important pour les animations CSS rapides utilisant requestionAnimationFrame(). crbug.com/808933 suit les travaux supplémentaires sur les performances dans Blink.

  • Traitement des erreurs De nouvelles méthodes d'analyse apportent la gestion des erreurs dans le monde du CSS.

  • "Dois-je utiliser des noms ou des chaînes CSS en camel case ?" Vous n'avez plus besoin de deviner si les noms sont en camel case ou des chaînes (par exemple, el.style.backgroundColor ou el.style['background-color']). Les noms de propriétés CSS dans Typed OM sont toujours des chaînes, ce qui correspond à ce que vous écrivez réellement en CSS :)

Compatibilité avec les navigateurs et détection des fonctionnalités

Le Typed OM a été intégré à Chrome 66 et est en cours d'implémentation dans Firefox. Edge a montré 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 usines numériques CSS.* est définie :

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

Principes de base des API

Accéder aux styles

Dans le modèle objet typé CSS, les valeurs sont distinctes des unités. 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 CSSOM

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

Nouveau Typed OM

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

Limitation / Arrondi des valeurs

L'une des fonctionnalités intéressantes du nouveau modèle d'objet est l'arrondi et/ou le clamping automatiques des valeurs de style calculées. Par exemple, imaginons que vous essayiez de définir opacity sur une valeur en dehors de la plage acceptable, [0, 1]. Les clamps OM typés limitent la valeur à 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, la valeur est arrondie à 15 et reste donc 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 Typed OM :

  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')), la plupart du temps, vous utiliserez 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 les spécifications 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(), 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 devient un peu déroutante. Voici quelques exemples pour vous aider à démarrer. J'ai ajouté une indentation supplémentaire pour les rendre plus faciles à lire.

calc(1px - 2 * 3em) se construit comme suit :

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

calc(1px + 2px + 3px) se construit comme suit :

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

calc(calc(1px + 2px) + 3px) se construit 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 est que vous pouvez effectuer des opérations mathématiques sur les 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 unités de longueur :

// 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 CSSTransformValue et en transmettant un tableau de valeurs de transformation (par exemple, CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Par exemple, supposons que vous souhaitiez recréer ce CSS :

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

Traduction en OM typé :

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 son caractère verbeux (lol !), CSSTransformValue propose des fonctionnalités intéressantes. Il possède une propriété booléenne pour différencier les transformations 2D et 3D, ainsi qu'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

Prenons un exemple pratique d'utilisation des transformations. Nous allons utiliser JavaScript et les transformations 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 nous permettent d'incrémenter l'angle directement à l'aide d'une opération mathématique.
  2. Plutôt que de toucher le DOM ou de relire une valeur à chaque frame (par exemple, pas de box.style.transform=`rotate(0,0,1,${newAngle}deg)`), l'animation est pilotée par la mise à jour de l'objet de données CSSTransformValue sous-jacent, ce qui améliore les performances.

Démo

Vous verrez un cube rouge ci-dessous si votre navigateur est compatible avec Typed OM. Le cube commence à tourner lorsque vous pointez dessus. L'animation est optimisée par CSS Typed OM. 🤘

Valeurs des propriétés personnalisées CSS

Les var() CSS deviennent un objet CSSVariableReferenceValue dans le modèle objet 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 étapes :

<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, comme 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 les valeurs

L'OM typé introduit des méthodes d'analyse dans la plate-forme Web. Cela signifie que vous pouvez enfin analyser les valeurs CSS de manière programmatique, avant de les utiliser ! Cette nouvelle fonctionnalité peut vous sauver la vie en vous permettant de détecter les bugs et le code CSS mal formé à un stade précoce.

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 acceptera cette valeur transform :

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

Conclusion

C'est agréable d'avoir enfin un modèle d'objet mis à jour pour CSS. Je n'ai jamais été à l'aise avec les chaînes de caractères. L'API CSS Typed OM est un peu verbeuse, mais elle devrait permettre de réduire le nombre de bugs et d'obtenir un code plus performant à terme.