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 StylePropertyMap
s 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
xel.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:
CSSUnitValue
: valores que contêm um único tipo de unidade (por exemplo,"42px"
).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:
- Os valores numéricos significam que podemos incrementar o ângulo diretamente usando a matemática.
- 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 dadosCSSTransformValue
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.