Cómo trabajar con el nuevo modelo de objetos escritos en CSS

A modo de resumen

Ahora, CSS tiene una API basada en objetos adecuada para trabajar con valores en JavaScript.

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

¡Se acabaron los días de concatenar cadenas y los errores sutiles!

Introducción

CSSOM anterior

CSS tiene un modelo de objetos (CSSOM) desde hace muchos años. De hecho, cada vez que lees o estableces .style en JavaScript, lo estás usando:

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

Nuevo CSS Typed OM

El nuevo CSS Typed Object Model (Typed OM), que forma parte del esfuerzo de Houdini, amplía esta visión del mundo agregando tipos, métodos y un modelo de objetos adecuado a los valores de CSS. En lugar de cadenas, los valores se exponen como objetos de JavaScript para facilitar la manipulación eficiente (y lógica) del CSS.

En lugar de usar element.style, accederás a los diseños a través de una nueva propiedad .attributeStyleMap para los elementos y una propiedad .styleMap para las reglas de hojas de diseño. Ambos devuelven un objeto 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');

Dado que los objetos StylePropertyMap son similares a los objetos Map, admiten todos los métodos habituales (get/set/keys/values/entries), lo que los hace flexibles para trabajar con ellos:

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

Ten en cuenta que, en el segundo ejemplo, opacity se establece como cadena ('0.3'), pero se devuelve un número cuando se vuelve a leer la propiedad más adelante.

Beneficios

Entonces, ¿qué problemas intenta resolver el CSS Typed OM? Si observas los ejemplos anteriores (y el resto de este artículo), podrías argumentar que el CSS Typed OM es mucho más detallado que el modelo de objetos anterior. ¡Estoy de acuerdo!

Antes de descartar el OM con escritura, considera algunas de las funciones clave que aporta:

  • Menos errores, p. ej., los valores numéricos siempre se devuelven como números, no como cadenas.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Operaciones aritméticas y conversión de unidades: Realiza conversiones entre unidades de longitud absoluta (p. ej., px -> cm) y operaciones matemáticas básicas.

  • Ajuste y redondeo de valores. El OM escrito redondea o ajusta los valores para que se encuentren dentro de los rangos aceptables de una propiedad.

  • Mejor rendimiento El navegador tiene que hacer menos trabajo para serializar y deserializar valores de cadena. Ahora, el motor usa una comprensión similar de los valores de CSS en JS y C++. Tab Atkins mostró algunos primeros comparativos de rendimiento que indican que el OM con tipos es alrededor de un 30% más rápido en operaciones por segundo en comparación con el uso del CSSOM y las cadenas anteriores. Esto puede ser importante para las animaciones CSS rápidas que usan requestionAnimationFrame(). crbug.com/808933 hace un seguimiento del trabajo de rendimiento adicional en Blink.

  • Manejo de errores Los nuevos métodos de análisis incorporan el control de errores al mundo de CSS.

  • "¿Debo usar nombres o cadenas de CSS en formato camel case?" Ya no es necesario adivinar si los nombres están en formato camelCase o son cadenas (p. ej., el.style.backgroundColor vs. el.style['background-color']). Los nombres de las propiedades de CSS en el OM con tipos siempre son cadenas, lo que coincide con lo que realmente escribes en CSS :)

Compatibilidad con navegadores y detección de funciones

El OM escrito se lanzó en Chrome 66 y se está implementando en Firefox. Edge mostró indicios de compatibilidad, pero aún no lo agregó a su panel de la plataforma.

Para la detección de funciones, puedes verificar si se define una de las fábricas numéricas CSS.*:

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

Conceptos básicos de API

Cómo acceder a los estilos

Los valores están separados de las unidades en el DOM con tipos de CSS. Obtener un diseño devuelve un objeto CSSUnitValue que contiene un objeto value y un objeto 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

Estilos calculados

Los estilos calculados se trasladaron de una API en window a un nuevo método en HTMLElement, computedStyleMap():

CSSOM antiguo

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

New Typed OM

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

Restricción o redondeo de valores

Una de las funciones interesantes del nuevo modelo de objetos es el ajuste o redondeo automático de los valores de estilo calculados. Por ejemplo, supongamos que intentas establecer opacity en un valor fuera del rango aceptable, [0, 1]. Los topes de OM escritos fijan el valor en 1 cuando se calcula el estilo:

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

Del mismo modo, establecer z-index:15.4 redondea a 15 para que el valor siga siendo un número entero.

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.

Valores numéricos de CSS

Los números se representan con dos tipos de objetos CSSNumericValue en el OM con escritura:

  1. CSSUnitValue: Valores que contienen un solo tipo de unidad (p.ej., "42px").
  2. CSSMathValue: Son valores que contienen más de un valor o unidad, como una expresión matemática (p.ej., "calc(56em + 10%)").

Valores de unidad

Los valores numéricos simples ("50%") se representan con objetos CSSUnitValue. Si bien podrías crear estos objetos directamente (new CSSUnitValue(10, 'px')), la mayoría de las veces usarás los métodos de fábrica 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 la especificación para ver la lista completa de métodos de CSS.*.

Valores matemáticos

Los objetos CSSMathValue representan expresiones matemáticas y, por lo general, contienen más de un valor o unidad. El ejemplo común es crear una expresión calc() de CSS, pero existen métodos para todas las funciones de CSS: calc(), min() y 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)"

Expresiones anidadas

Usar las funciones matemáticas para crear valores más complejos se vuelve un poco confuso. A continuación, se incluyen algunos ejemplos para comenzar. Agregué una sangría adicional para que sean más fáciles de leer.

calc(1px - 2 * 3em) se construiría de la siguiente manera:

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

calc(1px + 2px + 3px) se construiría de la siguiente manera:

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

calc(calc(1px + 2px) + 3px) se construiría de la siguiente manera:

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

Operaciones aritméticas

Una de las funciones más útiles del CSS Typed OM es que puedes realizar operaciones matemáticas en objetos CSSUnitValue.

Operaciones básicas

Se admiten las operaciones básicas (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))"

Conversión

Las unidades de longitud absoluta se pueden convertir a otras unidades de longitud:

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

Igualdad

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

Valores de transformación de CSS

Las transformaciones de CSS se crean con CSSTransformValue y se pasa un array de valores de transformación (p.ej., CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Por ejemplo, supongamos que deseas volver a crear este CSS:

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

Se tradujo a OM con escritura:

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

Además de su verbosidad (¡lol!), CSSTransformValue tiene algunas funciones interesantes. Tiene una propiedad booleana para diferenciar las transformaciones 2D y 3D, y un método .toMatrix() para devolver la representación DOMMatrix de una transformación:

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

Ejemplo: Animación de un cubo

Veamos un ejemplo práctico del uso de transformaciones. Usaremos JavaScript y transformaciones de CSS para animar 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.
})();

Observa lo siguiente:

  1. Los valores numéricos significan que podemos incrementar el ángulo directamente con operaciones matemáticas.
  2. En lugar de tocar el DOM o leer un valor en cada fotograma (p.ej., sin box.style.transform=`rotate(0,0,1,${newAngle}deg)`), la animación se controla actualizando el objeto de datos CSSTransformValue subyacente, lo que mejora el rendimiento.

Demostración

A continuación, verás un cubo rojo si tu navegador admite Typed OM. El cubo comienza a rotar cuando colocas el mouse sobre él. La animación está potenciada por el CSS Typed OM. 🤘

Valores de las propiedades personalizadas de CSS

Los var() de CSS se convierten en un objeto CSSVariableReferenceValue en el DOM con tipos. Sus valores se analizan en CSSUnparsedValue porque pueden tomar cualquier tipo (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 deseas obtener el valor de una propiedad personalizada, debes hacer lo siguiente:

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

Valores de posición

Las propiedades de CSS que toman una posición X/Y separada por espacios, como object-position, se representan con objetos 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

Análisis de valores

El OM con escritura introduce métodos de análisis en la plataforma web. Esto significa que, finalmente, puedes analizar valores de CSS de forma programática antes de intentar usarlos. Esta nueva capacidad puede ser muy útil para detectar errores y CSS con formato incorrecto en etapas tempranas.

Analiza un diseño 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)'

Analiza los valores en CSSUnitValue:

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

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

Manejo de errores

Ejemplo: Comprueba si el analizador de CSS aceptará este valor de transform:

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

Conclusión

Es bueno tener finalmente un modelo de objetos actualizado para CSS. Trabajar con cadenas nunca me resultó cómodo. La API del CSS Typed OM es un poco detallada, pero esperamos que genere menos errores y un código más eficiente en el futuro.