CSSOM - Why Houdini And The CSS Typed OM Is A Necessity
Tags: houdini, cssom, javascript
Published: 2019-08-06 04:59:40

Let's start with the basics. What is CSSOM?
The *OM* in CSSOM stands for **O**bject **M**odel. It's how JS interfaces with CSS in the DOM.

You're probably familiar with CSSOM, but maybe not the name. It's this:

```js
const div = document.querySelector('div');
div.style.marginLeft = '20px';
div.style["padding-right"] = '5px';
div.style.transform = 'translate(20px, 10px)';
```

See? The CSS is represented in an Object Model which lets us edit values and see them reflected in real time. We can set styles using good ole JS camel case (`marginLeft`), or CSS kebab case (`margin-left`).
<br/>

# Messy CSSOM

The CSSOM is fine and dandy in most instances, but anyone who has worked with it for long enough knows that there are situations where the worse side rears its ugly head. Let's create some of those situations.

```js
const div = document.querySelector('div');
div.style.position = 'relative';
div.style.left = '10' + (Math.random() > .5 ? 'px' : 'rem');
```

Now we have moved our div either 10px or 10rem to the right. Let's say we want to move it 10 additional units to the right, regardless of the unit type.

```js
const num = parseFloat(div.style.left) + 10;
const unit = div.style.left.slice(String(num).length);
div.style.left = num + unit;
``` 

At first I'm using a `parseFloat` trick to just get the number, but when we want to append the unit, it gets a bit trickier. Since `rem`, unlike most other unit values, has a length of three, we're using a different method which can handle any unit length.

It works, but it's definitely not clean or very predictable. Let's show another case.

```js
const div = document.querySelector('div');
const randLightness = Math.floor(Math.random() * 50) + 30; // between 30 and 79
div.style.background = `hsl(100, 100%, ${randLightness}%)`;
```

Now let's retrieve the color and get its lightness value.

```js
const lightness = div.style.background.split(',')[2].slice(1, -1);
console.log('lightness: ' + lightness); // > "lightness: 0"
```

That's weird. We put in a lightness of 30% - 79% and got back "0". There's no percentage either. I know my JS is right. I think so, at least. Let's console log the color and see what we're getting.

```js
console.log(div.style.background) // > "rgb(68, 204, 0)"
```

What? We put in an `hsl()` format. We seem to be getting back an `rgb()` representation of `hsl(100, 100%, 40%)`. If we want to increase the lightness by ten, we're going to have to try a much different method than last time.
<br/>

# CSSOM's Hazardous Consistencies 

I touched on this in a [previous article of mine](https://dev.to/emnudge/the-hazards-of-passing-css-through-js-a-look-into-css-transform-and-inconsistencies-44n0) surrounding a FireFox problem with `transform: translate()`.

The CSSOM has a spec which describes how it handles values passed into it. It will convert any color format into `rgb()` when it can. It's specified as follows:

> If <color> is a component of a resolved or computed value, then return the color using the rgb() or rgba() functional notation as follows

The "as follows" part isn't very interesting aside from how it specifies that if you pass `rgb()` an alpha parameter, it will convert it to `rgba()`. What's important is how it will always return a color in `rgb()` or `rgba()` format.

This is horrible if we want to edit a property using the `hsl()` format. We'd have to use complex math to convert it to `hsl()` before editing and sending it back.

The one way to prevent a lot of hassle is to use a [CSS variable](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) in place of one of the parameters. This prevents the engine from parsing it, since that would lead to much different results.

```js
div.style.background = 'hsl(100, 100%, calc(var(--lightness) * 1%))';
const randLightness = Math.floor(Math.random() * 50) + 30; // between 30 and 79
div.style.setProperty('--lightness', randLightness);
```

**NOTE**: You must use `.setProperty` to edit CSS variables. Setting it on the style property itself using `style['--lightness']` will not work. You can verify this by logging `div.style.cssText` when setting either.

But using CSS variables seems more like a patch than a solution. The CSS itself looks a bit messy and if a third party wanted to edit our CSS they'd have to recognize the existence of the CSS variable in order to make any changes.

The CSSOM makes a lot of changes like these. You can play around and see what gets changed by going to [this site by Tom Hodgins](https://tomhodgins.com/demo/cssom/). It's a fantastic little tool that lets you see how the engine parses your CSS in real time. It's primarily used to see if your browser supports a specific feature, but this is an excellent use case as well.
**Nov 21, 2019 edit:** About 5 minutes ago, we have come under the realization that he is CSSTOM Hodgins. Please address him as such in future encounters. 


This CSSOM behavior gets even messier when changing values of css properties that accept multiple parameters. 
<br/>

# CSSOM's Hazardous Cleanup

According to #2.2 in section [6.7.2 - Serializing CSS Values in the spec](https://drafts.csswg.org/cssom/#serializing-css-values):

> If component values can be omitted or replaced with a shorter representation without changing the meaning of the value, omit/replace them.

If possible, CSS values are cleaned up by replacing and omitting parts to make them shorter. This seems like a good idea in theory, but when passing values that can be shortened, they almost always will.

Using the [same tool](https://tomhodgins.com/demo/cssom/) mentioned in last section, we can verify that `margin: 1px 1px;` will be shortened to `margin: 1px;` in every browser, but `transform: translate(20px, 0px);` will only be shortened to `transform: translate(20px);` in firefox. 

Each of these shortenings have the exact same functionality in all browsers, but sometimes only one browser will choose to shorten it.

Due to the spec, this issue with FireFox was declared not to be a bug in response to a report on [Bugzilla](https://bugzilla.mozilla.org).

> As stated [on the spec], we may change this for compatibility with other engines, but I think lacking broken sites, the Gecko behavior is preferable / more consistent / more correct.

This all means that when we grab values from CSSOM, we have to be wary that it might not contain all the parameters we originally passed into it. This leads to a lot more logic and messy-looking code each time.

Luckily, the Houdini initiative aims to solve all of this. It currently does not address *all* of our problems, but hopes to soon do so.
<br />

# Introducing CSS Typed OM

Like how JS has the types 'String', 'Number', 'Boolean', etc, CSSOM is getting its mini type system as well. They are accessible through the global `CSS` variable and are used as follows:

```js
const div = document.querySelector('div');
div.attributeStyleMap.set('margin-left', CSS.px(23));
const margin = div.attributeStyleMap.get('margin-left')
console.log('margin-left: ' + margin); // > "margin-left: 23px"
```

Now this is certainly more verbose and CSS Typed OM *will* often be more verbose, but the typed system is a lot more safe, as types usually are.

The new way to access styles is through a [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)-like object called `attributeStyleMap` instead of just `style`. Like Maps, it has all the usual methods: get, set, has, etc. It's also an iterator, so it's loopable in a `for ...of`.

When retrieving the style, a bit of formatting goes on. You get an object containing a `value` and `unit` property. When calling the `.toString()` method, you get a concatenated string instead of an object. Concatenating the unit object will call the `.toString()` method, which is why the example above didn't include it.

How does this solve our problems? Let's start with one of the basic principles:
What you put in is what you get back. It does a lot of formatting for us so that our CSS is valid, but we can still retrieve what we put in.
Let's set up an example:
```js
const div = document.querySelector('div');
div.attributeStyleMap.set('z-index', CSS.number(4.45143));
```
What we have here is a z-index with a really peculiar z-index. Let's say that we're animating our z-index in a very specific fashion, so these decimal places are important to us.
CSSTOM will format this for us and truncate off the decimals, but when we request it later, our decimals will be preserved

```js
div.attributeStyleMap.get('z-index').values[0].value;  // -> 4.45143
div.computedStyleMap().get('z-index').value;           // -> 4
```
*Note 1*: We can get the parsed value using `.computedStyleMap()` instead of `.attributeStyleMap`.

*Note 2*: I used `.values[0].value` instead of `.value` since at the time of writing this article, there seems to be a bug where decimal values in CSSTOM z-index will generate a CSSMathSum object.

This behavior will in the future (I got confirmation from [Tab Atkins Jr.](https://github.com/tabatkins)) also extend to colors. The value will still be parsed into `rgb()` format, but we will be able to retrieve our `hsl()` representation. 

Transform functions are also much more organized. We are additionally going to be getting a separate `translate` and `scale` css property for even better access in the future.

# CSSTOM Now

At the time of writing this article, CSS Typed OM is only available in part on Chrome (Chromium, so Opera too and soon Edge) exclusively. The spec itself is still being written, so it may be some time before we see a full implementation.

In my opinion, CSSTOM is the only Houdini initiative that really **fixes** something. The other features coming along are absolutely fantastic as well, but more about additions than fixes. CSSTOM cannot come any sooner.