Como trabalhar com o novo modelo de objeto tipado do CSS

Texto longo, leia o resumo

O CSS agora tem uma API baseada em objeto adequada para trabalhar com valores em JavaScript.

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

Os dias de concatenação de strings e bugs sutis acabaram!

Introdução

CSSOM antigo

O CSS tinha um modelo de objeto (CSSOM, na sigla em inglês) há muitos anos. Na verdade, sempre que você ler/definir .style no JavaScript, ele será usado:

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

Novo OM tipado de CSS

O novo modelo de objeto tipado de CSS (OM, na sigla em inglês), parte do esforço da Houdini, expande essa visão do mundo adicionando tipos, métodos e um modelo de objeto adequado aos valores do CSS. Em vez de strings, os valores são expostos como objetos JavaScript para facilitar a manipulação eficiente e sensata do CSS.

Em vez de usar element.style, você acessa os estilos usando uma nova propriedade .attributeStyleMap para elementos e uma propriedade .styleMap para regras de folha de estilo. Ambas retornam um 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');

Como as StylePropertyMaps são objetos semelhantes a mapas, elas oferecem suporte a todos os suspeitos habituais (get/set/keys/values//entradas), tornando-os flexíveis para o trabalho:

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

No segundo exemplo, opacity é definido como string ('0.3'), mas um número é retornado quando a propriedade é lida mais tarde.

Vantagens

Quais problemas o OM tipado de CSS está tentando resolver? Analisando os exemplos acima (e ao longo do restante deste artigo), você pode argumentar que o OM tipado de CSS é muito mais detalhado do que o modelo de objeto antigo. Eu concordo!

Antes de cancelar o OM tipado, considere alguns dos principais recursos que ele traz para a tabela:

  • Menos bugs (por exemplo, valores numéricos sempre são retornados como números, não strings).

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Operações aritméticas e conversão de unidades: converta entre unidades de comprimento absoluto (por exemplo, px -> cm) e faça cálculos básicos.

  • Limitação e arredondamento de valor. Valores de Rounds e/ou clamps de OM tipados para que estejam dentro dos intervalos aceitáveis para uma propriedade.

  • Melhor desempenho. O navegador precisa realizar menos trabalho de serialização e desserialização de valores de string. Agora, o mecanismo usa um entendimento semelhante de valores CSS em JS e C++. A Tab Akins mostrou alguns comparativos de mercado de desempenho inicial que colocam o OM tipado em ~30% mais rápido em operações/s quando comparado ao uso do CSSOM e strings antigos. Isso pode ser significativo para animações CSS rápidas que usam requestionAnimationFrame(). O crbug.com/808933 acompanha outros trabalhos de desempenho no Blink.

  • Tratamento de erros. Os novos métodos de análise trazem o tratamento de erros no mundo do CSS.

  • "Devo usar strings ou nomes CSS com letras concatenadas?" Não há mais como adivinhar se os nomes têm letras concatenadas ou strings (por exemplo, el.style.backgroundColor x el.style['background-color']). Os nomes de propriedades CSS no OM tipado são sempre strings, correspondentes ao que você realmente escreve no CSS :)

Suporte a navegadores e detecção de recursos

O OM tipado chegou ao Chrome 66 e está sendo implementado no Firefox. O Edge mostrou sinais de suporte, mas ainda não o adicionou ao painel da plataforma.

Para a detecção de recursos, verifique se uma das fábricas numéricas CSS.* está definida:

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

Princípios básicos da API

Acesso a estilos

Os valores são separados das unidades no OM tipado CSS. Receber um estilo retorna um CSSUnitValue contendo 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

Estilos calculados

Os estilos calculados foram movidos de uma API em window para um novo método em HTMLElement, computedStyleMap():

CSSOM antigo

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

Novo OM tipado

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

Limitação / arredondamento de valor

Um dos recursos interessantes do novo modelo de objeto é fixação automática e/ou arredondamento de valores de estilo calculados. Por exemplo, digamos que você tente definir opacity como um valor fora do intervalo aceitável, [0, 1]. O OM digitado fixa o valor para 1 ao calcular o 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.

Da mesma forma, definir z-index:15.4 arredonda para 15 para que o valor permaneça um número inteiro.

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

Os números são representados por dois tipos de objetos CSSNumericValue em OM tipado:

  1. CSSUnitValue: valores que contêm um único tipo de unidade (por exemplo, "42px").
  2. CSSMathValue: valores que contêm mais de um valor/unidade, como expressão matemática (por exemplo, "calc(56em + 10%)").

Valores unitários

Valores numéricos simples ("50%") são representados por objetos CSSUnitValue. Embora seja possível criar esses objetos diretamente (new CSSUnitValue(10, 'px')), na maioria das vezes, você usará os 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'

Consulte a especificação para a lista completa de métodos CSS.*.

Valores matemáticos

Os objetos CSSMathValue representam expressões matemáticas e normalmente contêm mais de um valor/unidade. O exemplo comum é a criação de uma expressão CSS calc(), mas existem métodos para todas as funções 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)"

Expressões aninhadas

Usar as funções matemáticas para criar valores mais complexos é um pouco confuso. Veja abaixo alguns exemplos para você começar. adicionei um recuo extra para facilitar a leitura.

calc(1px - 2 * 3em) seria criado como:

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

calc(1px + 2px + 3px) seria criado como:

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

calc(calc(1px + 2px) + 3px) seria criado como:

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

Operações aritméticas

Um dos recursos mais úteis do OM tipado de CSS é que é possível executar operações matemáticas em objetos CSSUnitValue.

Operações básicas

As operações básicas (add/sub/mul/div/min/max) têm suporte:

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

Conversão

As unidades de comprimento absoluto podem ser convertidas em outros comprimentos de unidade:

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

Igualdade

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 transformação de CSS

As transformações CSS são criadas com um CSSTransformValue e transmitindo uma matriz de valores de transformação (por exemplo, CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Como exemplo, digamos que você queira recriar este CSS:

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

Traduzido para OM tipado:

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

Além do nível de detalhes (lolz!), O CSSTransformValue tem alguns recursos interessantes. Ele tem uma propriedade booleana para diferenciar transformações 2D e 3D e um método .toMatrix() para retornar a representação DOMMatrix de uma transformação:

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

Exemplo: animação de um cubo

Vamos conferir um exemplo prático de uso de transformações. Vamos usar transformações JavaScript e CSS para animar um 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.
})();

Observe que:

  1. Os valores numéricos significam que podemos incrementar o ângulo diretamente usando a matemática.
  2. Em vez de tocar no DOM ou ler um valor em cada frame (por exemplo, sem box.style.transform=`rotate(0,0,1,${newAngle}deg)`), a animação é conduzida atualizando o objeto de dados CSSTransformValue subjacente, melhorando a performance.

Demonstração

Abaixo, você verá um cubo vermelho se o navegador for compatível com OM tipado. O cubo começa a girar quando você passa o mouse sobre ele. A animação usa a tecnologia do tipo OM do CSS! 🤘

Valores de propriedades personalizadas do CSS

O var() do CSS se torna um objeto CSSVariableReferenceValue no OM tipado. Os valores delas são analisados em CSSUnparsedValue porque podem assumir qualquer 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'

Para receber o valor de uma propriedade personalizada, é preciso fazer algumas tarefas:

<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 posição

As propriedades CSS que ocupam uma posição x/y separada por espaço, como object-position, são representadas por 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

Como analisar valores

O OM tipado introduz métodos de análise na plataforma da Web. Isso significa que é possível finalmente analisar os valores CSS de maneira programática, antes de tentar usá-los. Esse novo recurso é uma possível economia para detectar bugs iniciais e CSS malformado.

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

Analise valores em CSSUnitValue:

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

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

Tratamento de erros

Exemplo: verifique se o analisador de CSS ficará satisfeito com este valor transform:

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

Conclusão

É bom finalmente ter um modelo de objeto atualizado para CSS. Trabalhar com strings nunca foi ideal para mim. A API CSS Typed OM é um pouco detalhada, mas esperamos que isso resulte em menos bugs e em um código de melhor desempenho no futuro.