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

Resumen

CSS ahora 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 concatenación de cadenas y los errores sutiles.

Introducción

CSSOM anterior

CSS tiene un modelo de objetos (CSSOM) durante muchos años. De hecho, cada vez que leas o establezcas .style en JavaScript, lo usarás:

// 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 OM escrito en CSS

El nuevo Modelo de objetos escritos de CSS (OM escrito), 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 una manipulación de CSS de alto rendimiento (y sensata).

En lugar de usar element.style, accederás a los estilos a través de una nueva propiedad .attributeStyleMap para los elementos y una propiedad .styleMap para las reglas de la hoja de estilo. Ambos muestran 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');

Debido a que las StylePropertyMap son objetos similares a un mapa, admiten todos los sospechosos habituales (get/set/keys/values/ingress), lo que las hace flexibles para trabajar con lo siguiente:

// 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 una cadena ('0.3'), pero se muestra un número cuando la propiedad se vuelve a leer más adelante.

Beneficios

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

Antes de cancelar OM escrito, considera algunas de las características clave que aporta a la tabla:

  • Menos errores. P.ej., los valores numéricos siempre se muestran como números, no como strings.

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

  • Restricción y redondeo de valores. Valores de redondeos o restricciones de OM escritos para que estén dentro de los rangos aceptables para una propiedad

  • Mejor rendimiento. El navegador debe realizar menos trabajo de serializar y deserializar los valores de la string. Ahora, el motor utiliza una comprensión similar de los valores de CSS en JS y C++. Tab Akins mostró algunas comparativas de rendimiento iniciales que colocan OM en ~30% más rápido en operaciones por segundo en comparación con el uso del CSSOM y las cadenas anteriores. Esto puede ser significativo para animaciones de CSS rápidas que usan requestionAnimationFrame(). crbug.com/808933 realiza un seguimiento del trabajo de rendimiento adicional en Blink.

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

  • "¿Debo usar cadenas o nombres CSS en mayúsculas y minúsculas?" Ya no tienes que adivinar si los nombres tienen mayúsculas mediales o cadenas (p. ej., el.style.backgroundColor frente a el.style['background-color']). Los nombres de las propiedades de CSS en Typed OM siempre son cadenas que coinciden con lo que realmente escribes en CSS :).

Detección de funciones y compatibilidad con navegadores

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

Para la detección de características, puedes verificar si se definió 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 son independientes de las unidades en OM escrito en CSS. Si obtienes un diseño, se muestra un CSSUnitValue que contiene un value y 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

Estilos calculados

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

CSSOM anterior

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

Nuevo OM escrito

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

Restricción o redondeo de valores

Una de las ventajas del nuevo modelo de objetos es la fijación o el redondeo automáticos de los valores de estilo calculados. A modo de ejemplo, supongamos que intentas establecer opacity en un valor fuera del rango aceptable, [0, 1]. El OM escrito restringe el valor a 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, la configuración de z-index:15.4 se redondea a 15, por lo que el valor sigue 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 Typed OM:

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

Valores unitarios

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 las especificaciones para ver la lista completa de métodos CSS.*.

Valores matemáticos

Los objetos CSSMathValue representan expresiones matemáticas y suelen contener más de un valor por 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

El uso de las funciones matemáticas para crear valores más complejos se vuelve un poco confuso. Estos son algunos ejemplos que pueden servirte para comenzar. agregué 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 OM escrito de CSS 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 en otras longitudes de unidad de la siguiente manera:

// 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 una CSSTransformValue y pasan un array de valores de transformación (p.ej., CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Por ejemplo, supongamos que deseas volver a crear esta CSS:

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

Traducido al OM escrito:

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 (¡lolz!), CSSTransformValue tiene algunas funciones interesantes. Tiene una propiedad booleana para diferenciar las transformaciones 2D de las 3D, y un método .toMatrix() para mostrar 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: Cómo animar un cubo

Veamos un ejemplo práctico del uso de transformaciones. Usaremos transformaciones de JavaScript y 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 aumentar el ángulo directamente usando 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 impulsa mediante la actualización del objeto de datos CSSTransformValue subyacente, lo que mejora el rendimiento.

Demostración

A continuación, verás un cubo rojo si tu navegador es compatible con Typed OM. El cubo comienza a girar cuando pasas el mouse sobre él. La animación funciona con CSS Typed OM. 🤘

Valores de las propiedades personalizadas de CSS

var() de CSS se convierte en un objeto CSSVariableReferenceValue en Typed OM. Sus valores se analizan en CSSUnparsedValue porque pueden tomar cualquier tipo (px, %, em, rgba(), etcétera).

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 realizar algunas tareas:

<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 escrito presenta métodos de análisis en la plataforma web. Esto significa que puedes analizar los valores de CSS de manera programática, antes de intentar usarlos. Esta nueva función es un posible ahorro de vida si se detectan errores tempranos y CSS con errores de formato.

Analiza un estilo 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 estará conforme con este valor de transform:

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

Conclusión

Por fin, es bueno tener un modelo de objetos actualizado para CSS. Trabajar con cadenas nunca me sentía bien. La API de OM Typed de CSS es un poco detallada, pero esperamos que genere menos errores y un código con mejor rendimiento más adelante.