1. Introduction
The current [css-typed-om-1] approach to unitted valued is incoherent and inextensible. There are three distinct object "shapes" employed right now, and we’re only handling two "types" of units - lengths and angles. The inconsistency makes it difficult for authors to predict how to interact with unitted values, and limits our ability to innovate in the future. The overall approach means we have to invent a large number of different interfaces to handle all the different types of unitted values CSS has, and completely fails to handle the planned addition of custom unit types.
1.1. Inconsistency
We’re currently using three different "shapes" for unitted value objects:
-
"Simple" lengths (corresponding to values like
5px
or2em
) have a {value, unit} structure. -
"Complex" lengths (corresponding to a
calc()
expressions) have a {px, pc, in, em, vw, ...} structure, where each field is an independent entry in the sum-of-values that makes up the calc(). -
"Simple" angles have a {deg, rad, grad, turn} structure, where each field is linked; they all reflect a hidden internal value, so you can write to any field and then read the equivalent value in a different unit from another field. (Such as
x.deg = 180; print(x.turn) // .5
.)
1.2. Explosion of Types
While <length> has two types—calc()
—calc()
s can be resolved into a "simple" angle currently.
This ignores the possibility of a future property using <angle> and <percentage> together in a way that isn’t immediately resolvable (for example, audio properties that use % to convey left-to-right progress along the audio stage, which has an unknown width), or the addition of an <angle> unit that doesn’t have a fixed conversion ratio (like em or vw has for <length>).
The spec also currently ignores all the other types of units,
which will need to be represented in the Typed OM at some point.
They’ll result in a number of additional interfaces when we support them.
These interfaces will all need to decide whether they look like CSSSimpleLength
or CSSAngleValue
,
sometimes without any information about the type family
(for example, the <flex> type has only a single unit in it;
which variant should we choose?).
1.3. No User Extensibility
One of the recorded plans for a future Houdini API
is to allow authors to define custom units for themselves.
For example,
there’s still a mess of length units
used by publishers
which we don’t particularly want to add to core CSS
(we’ve already added several, like pc
and q
),
but which would be useful for authors using CSS for publishing.
The CSSSimpleLength
interface is compatible with custom units—.type
.
(We’ll have to relax the attribute into a DOMString,
and the constructor into (LengthType or DOMString)
,
but that’s easy.)
The CSSCalcLength
interface is also able to be extended to handle custom units;
we’ll have to extend it to handle complex units already
(when we allow unit algebra),
and a similar approach will allow arbitrary unit extensions
(a Map hanging off the side which contains arbitrary additional entries).
But the CSSAngleValue
interface doesn’t work at all,
at least not without some severe awkwardness.
If you hang a map off the side,
you need to have magical updating going on whenever you set a value,
which is difficult to spec right now
(because Maps don’t have a natural way to observe them).
And this doesn’t cover brand new units, which don’t map to any of the existing types, at all. They don’t have a corresponding OM class to live under.
2. Proposal
interface CSSDimension { CSSDimension add(CSSDimension value); CSSDimension subtract(CSSDimension value); CSSDimension multiply(double value); CSSDimension divide(double value); static CSSDimension from(DOMString cssText); static CSSDimension from(double value, DOMString type); CSSDimension to(DOMString type); }; interface CSSSimpleDimension : CSSDimension { attribute double value; attribute DOMString type; }; interface CSSCalcLength : CSSDimension { attribute double? px; attribute double? percent; // ... static CSSDimension from(optional CSSCalcLengthDictionary dictionary); }; interface CSSCalcAngle : CSSDimension { attribute double deg; attribute double rad; attribute double grad; attribute double turn; static CSSDimension from(optional CSSCalcAngleDictionary dictionary); }; // same for <time> and <frequency>, // the only other types allowed in calc() currently
All unitted values share the CSSSimpleDimension
interface.
Arithmetic on CSSDimension
values
throws if the value types aren’t compatible
(just like, today, they’d throw if you passed a CSSAngleValue
to add()
).
Otherwise, it returns the appropriate CSSCalc*
subclass.
The new to()
method converts from one unit to another.
It throws if the types aren’t convertible
(such as px
to deg
, but also px
to em
),
or if the object is a CSSCalc*
and some of its non-zero specified values aren’t convertible
(so CSSCalcLength({px:5, in:1}).to("px")
is fine,
but CSSCalcLength({px:5, em:1}).to("px")
throws).
Otherwise,
it returns a new CSSSimpleDimension
with the specified unit.
2.1. Consistency
All unitted values now use the same interface structure.
You don’t need to remember whether something is a <length> or <angle> to know how to get its value out,
or just guess at less-used unit types;
they’re all .value
.
All types that are allowed in calc() now have corresponding interfaces, again all with the same structure. As we expand calc() to allow new types, we’ll add new classes.
All types that can be converted
do so with a standard mechanism
(the to()
method),
which is short and easy to use.
2.2. Minimal Types
Rather than needing separate classes for every new kind of unit (and who knows about user-defined units), we have a single class for all "simple" unitted values, and one class per variety of type allowed in calc(), which is a small group that grows slowly.
2.3. User Extensibility
When user-defined units arrive,
they’ll be handled with the exact same mechanism as any other unit—
For calc()-allowed types,
all CSSCalc*
classes will use the same mechanism
to represent user-defined units
(a Map hanging off the side).
2.4. Cons
-
Converting between the types of angles is somewhat more difficult. Before you just read the attribute you want, like
x.rad
, now you have to dox.to("rad").value
. (On the plus side, you can now convert between all the absolute length units, which wasn’t previously possible.) -
You could previously tell by standard JS type-testing whether a given unitted value was a length or angle. That’s no longer possible unless it’s a calc() value; you have to be able to infer that from the unit now.
3. Alternatives
Instead of the calc() interfaces having explicit attributes for their known units,
with extra Maps eventually hanging off of them
for user-defined units
and complex units,
we could instead just make them Maplike. All units would then be treated identically;
you .get()
and .set()
them,
and can iterate over it to get the values.
This also lets us avoid any name collisions between CSS units
and future attributes or methods on the objects;
for example, in the current design we wouldn’t be able to ever create a to
unit,
as it would clash with the to()
method on the prototype.