Scala DOM Types
"com.raquo" %%% "domtypes" % "0.10.0" // Scala.js 1.x "com.raquo" %% "domtypes" % "0.10.0" // JVM
Our type definitions are designed for easy integration into any kind of library. You can use this project to build your own DOM libraries like React or Snabbdom, but type-safe. For example, popular Scala.js reactive UI library Outwatch recently switched to Scala DOM Types, offloading thousands of lines of code and improving type safety (diff). I am also using Scala DOM Types in my own projects:
- Laminar, a high level reactive UI library for Scala.js
- Scala DOM Builder, a low level DOM manipulation and tree tracking library
- Scala DOM Test Utils, a library that verifies that your DOM node / tree matches provided description
DOM stands for Document Object Model, in our context it's an object that represents an HTML document along with its HTML elements and their attributes, props and styles.
Table of Contents
- Why use Scala DOM Types
- What about ScalaTags
- What about scala-js-dom
- Design Goals
- Related Projects
Please use github issues for bugs, feature requests, as well as all kinds of discussions, including questions on usage and integrations. I think this will work better than spreading thin across gitter / stackoverflow / etc. You can watch this project on github to get issue updates if you're interested in following discussions.
See also: Contribution guide
Why use Scala DOM Types
Canonical use case: you're writing a Scala library that does HTML / DOM construction / manipulation and want to provide a type-safe API like this:
div( h1(rel := "title", "Hello world"), p( backgroundColor := "red", "Welcome to my fancy page!", span(draggable := true, "Fancyness is important.") ), button(onClick := doFancyThing, "Do Fancy Thing"), a(href := "http://example.com", title := "foo", "Example") )
Of course, your API doesn't need to look anything like this, that's just an example. Scala DOM Types doesn't actually provide the
:= methods that you'd need to make this example work.
If you do in fact want similar syntax, you could extend
Prop, etc., or provide your own alternatives to those (Scala DOM Types does not require you to use its own traits).
You can also extend the API of those classes with implicit conversions / implicit classes instead of subclassing. Or you might even use Scala DOM Builder if that's what you need, or some of its individual classes (it's also very extensible and reusable).
You don't need to be writing a whole library to benefit from Scala DOM Types, you can use it instead to make your application code more type-safe. For example, your imaginary method
setProperty(element: dom.Element, propName: String, propValue: Any)
setProperty[Value](element: dom.Element, prop: Prop[Value], propValue: Value)
Now you can't pass just about any random string as
propName, and even
propValue is now type checked.
What about ScalaTags
ScalaTags is a popular Scala library that contains DOM type definitions similar to what we have here. However, Scala DOM Types is different in a few ways:
More type safe. For example, in Scala DOM Types an
inputtag is linked to Scala.js
HTMLInputElementclass. This lets you provide exact types for the DOM nodes you create, so that you don't need to perform unsafe casts in your application code if you want to e.g. access the
valueproperty on an
inputyou created. Similarly, all attributes, properties and styles are linked to the types that they accept to prevent you from assigning incorrect values.
More flexible. Scala DOM Types does not tell you how to compose your attributes / props / styles / tags together, and does not enforce any rendering paradigm. You are free to implement your own composition. I see that some projects fork ScalaTags just to get the type definitions without everything else. Scala DOM Types does not get in your way, eliminating the need for such forking.
Better representation of native DOM types. Scala DOM Types handles Reflected Attributes consistently, and uses Codecs to properly encode/decode DOM values.
There are some other differences, for example Scala DOM Types uses camelCase for attr / prop / style names because that is consistent with common Scala style.
What about scala-js-dom
HTMLInputElement. You can use those types when you already have instances of DOM elements, but you can not instantiate those types without using untyped methods like
On the other hand, Scala DOM Types lets the consuming library create a type-safe representation of real JS DOM nodes or trees, and it is up to your library's code to instantiate real JS nodes from the provided description. Scala DOM Builder does that in the most straightforward way, but higher level libraries like React, Snabbdom or Laminar could use Scala DOM Types in their own way, e.g. to create virtual or reactive DOM structures.
Oh, and Scala DOM Types does work on the JVM. Obviously you can't get native JS types there, but you can provide your own replacements for specific Scala.js types, or just not bother with such specificity (see
The purpose of Scala DOM Types is to become a standard DOM types library used in Scala projects, both in Scala.js and the JVM.
The most important type information must be encoded as Scala types. For example, DOM properties that only accept integers should be typed as such.
Reasonably Precise Types
The types we provide will never be perfect. For example, MDN has this to say about the
list attribute (
listId in our API):
The value must be the id of a element in the same document. [...] This attribute is ignored when the type attribute's value is hidden, checkbox, radio, file, or a button type.
A far as I know, encoding such constraints as Scala types would be very hard, if possible at all.
This is not to say that we are content with the level of type safety we currently have in Scala DOM Types. Improvements are welcome as long as they provide significantly more value than burden to users of this library. This kind of thing is often subjective, so I suggest you open an issue for discussion first.
Scala DOM Types is a low level library that is used by other libraries. As such, its API should be unopinionated and focused solely on providing useful data about DOM elements / attributes / etc. to consuming libraries in a way that is easy for them to implement.
Sanity Preservation Measures
We should provide a better API than the DOM if we can do that in a way that keeps usage discoverable and unsurprising.
Developers familiar with the DOM API should generally be able to discover the names of attributes / tags / etc. they need using IDE autocompletion (assuming they expect the names to match the DOM API). For example:
forId is a good name for the
for attribute. It avoids using a Scala reserved word, and it starts with
for like the original attribute, so it's easy to find. It also implies what kind of string is expected for a value (an
id of an element).
Within that constraint, we should also try to clean up the more insane corners of the DOM API.
- For example, the difference between
valueproperty trips up even experienced developers all the time. Scala DOM Types on the other hand has a
defaultValuereflected attribute and a
valueproperty, which behave the way everyone would expect from the given names or from their knowledge of the DOM API.
- For another example, enumerated attributes like
contentEditablethat in the DOM accept "true" / "false" or "on" / "off" or "yes" / "no" should be boolean attributes in Scala DOM Types.
All naming differences with the DOM API should be documented in the README file (see below). Type differences are generally assumed to be self-documenting.
- Write about general project structure, builders, etc.
- Provide links to specific implementation examples in other libraries (use my keys + implicits, or use your own keys)
Scala DOM Types provides some normalization of the native HTML / DOM API, which is crazy in places.
For example, there are a few ways to encode a boolean value into an HTML attribute:
- As presence of the attribute – if attribute is present,
- As string "true" for true, or "false" for false
- As string "yes" for true, or "no" for false.
Which one of those you need to use depends on the attribute. For example, attribute
disabled needs option #1, but attribute
contenteditable needs option #2. And then there are DOM Properties (as opposed to HTML Attributes) where booleans are encoded as actual booleans.
Similarly, numbers are encoded as strings in attributes, with no such conversion when working with properties.
For example, the codecs for the three boolean options above are
BooleanAsYesNoStringCodec. They have concrete implementations of encode / decode methods but of course you don't have to use those.
HTML attributes and DOM properties are different things. As a prerequisite for this section, please read this StackOverflow answer first.
For more on this, read Section 2.6.1 of this DOM spec. Note that it uses the term "IDL attributes" to refer to what we call "DOM properties", and "Content attributes" to refer to what we here call "HTML attributes".
So with that knowledge,
id for example is a reflected attribute. Setting and reading it works exactly the same way regardless of whether you're using the HTML attribute
id, or the DOM property
id. Such reflected attributes live in
ReflectedHtmlAttrs trait, which lets you build either attributes or properties depending on what implementation of
ReflectedHtmlAttrBuilder you provide.
To keep you sane, Scala DOM Types reflected attributes also normalize the DOM API a bit. For example, there is no
value attribute in Scala DOM Types. There is only
defaultValue reflected attribute, which uses either the
value HTML attribute or the
defaultValue DOM property depending on how you implement
ReflectedHtmlAttrBuilder. This is because that attribute and that property behave the same even though they're named differently in the DOM, whereas the
value DOM property has different behaviour (see the StackOverflow answer linked above). A corresponding HTML attribute with such behaviour does not exist, so in Scala DOM Types the
value prop is defined in trait
Props. It is not an attribute, nor is it a a reflected attribute.
Reflected attributes may behave slightly differently depending on whether you implement them as props or attributes. For example, in HTML5 the
cols reflected attribute has a default value of
20. If you read the
col property from an empty
<textarea> element, you will get
20. However, if you try to read the attribute
col, you will get nothing because the attribute was never explicitly set.
className often require special handling in consuming libraries. For example, instead of a
String based interface, you might want to offer a
Seq[String] based one for
className. To facilitate the development of such opinionated APIs we offer these keys in separate traits (
ComplexSvgKeys) that allow for completely custom types.
If you don't need such customization at all, just use
CanonicalComplexSvgKeys traits which implement these keys similar to all the others.
DOM Events &
When listening to
onInput events found in
FormEventProps, you often need to access
event.target.value to get the new value of the input element the event was fired on. However,
dom.Event.target is an
EventTarget, whereas the
value property is only defined on
EventTarget is not.
target in JS events is hard because almost all events in which we care about it could fire not only on
HTMLInputElement, but also
HTMLTextAreaElement, and even
HTMLElement in some cases (
onInput on element with
contentEditable set to
Scala DOM Types provides a few type params in
FormEventProps to help deal with this mess, as well as the
TypedTargetEvent type refinement trait. Ultimately, you simply can't safely use
.target as something other than an
HTMLElement for most events due to the underlying JS API being very dynamic.
CSS is rather hard to type properly. A lot of CSS properties accept both numbers and a set of magic strings such as "auto", or specially formed strings in multiple formats such as "2px 5em 0 auto". We attempt to deal with this the same way that ScalaTags does, by defining objects for some CSS properties that have shorthand methods for applicable magic strings defined on them.
The downside of this approach is that this requires Scala DOM Types to venture outside of its design scope, as for such a setup to work Scala DOM Types needs to be aware of the concept of setters –
Modifiers that set a particular property to a particular value, such as
backGroundColor := "auto".
See Issue #2 for discussion.
SVG attributes have the same typing problems as CSS properties (see above), but a different solution in Scala DOM Types. We basically type most SVG attributes as strings. Eventually we hope to find a better solution that will fit both SVG and CSS use cases. See Issue #2 for discussion.
Naming Differences Compared To Native HTML & DOM
We try to make the native HTML & DOM API a bit saner to work with in Scala.
- All identifiers are camelCased, (e.g.
dataList) for consistency with conventional Scala style.
data-<suffix>attributes are created using
aria-<suffix>attributes are available without the
aria-prefix in the
AriaAttrstrait. You could thus create
object aria extends AriaAttrs[...]to namespace those attributes.
Attributes & Props
valueattribute is renamed
defaultValuebecause native naming is misleading and confusing (example)
- Note that the
valueproperty retains its name
- Note that the
checkedattribute is renamed to
defaultCheckedfor the same reason
- Note that the
checkedproperty retains its name
- Note that the
selectedattribute is renamed to
defaultSelectedfor the same reason
- Note that the
selectedproperty retains its name
- Note that the
classattribute is renamed to
classNamefor consistency with reflected property name, and to avoid Scala reserved word
htmlForproperty are available as reflected attribute
forIdfor consistency and to avoid Scala reserved word
idreflected attribute is renamed to
stepAttrto free up good names for end user code
resultSVG attributes renamed to
resultAttrrespectively to free up good names for end user code
styleattribute is renamed to
styleAttrto let you implement a custom
styleattribute if you want.
contentattribute is renamed to
contentAttrto avoid conflict with
formattribute is renamed to
formIdto avoid conflict with
heightattribute is renamed to
heightAttrto avoid conflict with
widthattribute is renamed to
widthAttrto avoid conflict with
listattribute is renamed to
listIdfor clarity and consistency
contextmenuattribute is renamed to
contextMenuIdfor clarity and consistency
- Tags renamed to free up good names for end user code:
- Other tag renamings:
titleTagto avoid conflict with
objectTagto avoid Scala reserved word
tpeto avoid Scala reserved word
clsfor consistency with Scala / ScalaTags
How to Use Scala DOM Types
First off, if you're building such a library, you need to know a quite few things about how JS DOM works. Scala DOM Types is just a collection of types, it's not an abstraction layer for the DOM. You're building the abstraction layer. We can't cover everything about JS DOM here, but we did touch on some of the nastier parts above.
Laminar is my own library that uses Scala DOM Types in a pretty standard way. Open Laminar.scala, which is Laminar's public API, and let's see what it has.
As you see we're building up a big
private[laminar] object Laminar. It's marked private because it's exposed under a different alias,
com.raquo.laminar.L. End users are expected to import either this object, or all of its properties (
L._) depending on their preference, everything will work either way.
object Laminar extends quite a few Scala DOM Types traits. Let's see what some of these traits these are, and what kinds of type params we pass to them.
with ReflectedHtmlAttrs[ReactiveReflectedProp] – this brings in the
ReflectedHtmlAttrs trait. The type param of
ReflectedHtmlAttrs indicates the type of Reflected Attribute instanced that you get when you call
L.alt etc. In Laminar, this type is
ReactiveProp which extends Scala DOM Types Prop class. So ReactiveProp adds Laminar functionality to Prop, but it doesn't actually have to extend Prop, Prop is only provided as a canonical Prop class to help potential interop between libraries and to reduce boilerplate.
Speaking of boilerplate,
ReflectedHtmlAttrs trait requires Laminar object to also implement
ReflectedHtmlAttrBuilder. Because Laminar uses its own
ReactiveProp type, we have to implement it ourselves, and we do it in
HtmlBuilders trait, which
object Laminar also extends. But if Laminar just used the canonical
Prop instead of its own
ReactiveProp, it could have simply used
Scala DOM Types CanonicalReflectedPropBuilder instead of re-implementing all the same
reflectedAttr method in
HtmlBuilders. But then I would need to add Laminar functionality to
Prop by means of implicit extension methods, and I personally prefer non-implicit APIs.
If you look at
HtmlBuilders you will see that it extends quite a few other traits – those follow the same pattern but for different types of keys – html attrs, event props, style props, tags, etc. And you'll see
object Laminar extending Scala DOM Types traits that provide these listings of attrs, event props, style props, tags, etc.
Lastly, Scala DOM Types code provides quite a few comments explaining what the traits are used for and what the classes represent. Hopefully together with the Laminar example it will be clear enough how to use Scala DOM Types from your library. If you've built libraries on top of ScalaTags, it's a somewhat similar pattern.
Notice that most Scala DOM Types def traits live in the
shared project. Some of them accept many type params to represent different types of DOM Events or HTML elements. This enables you to use Scala DOM Types on the JVM. For implementing a purely frontend library you will want to use the type aliases defined in the
js project, specifically in the package object there. Those aliases provide standard type params for Scala DOM Types definition traits from the scalajs-dom project. Laminar uses those type aliases, and there is no reason not to if you don't need JVM compatibility in your library.
My Related Projects
- Laminar – Reactive UI library based on Scala DOM Types
- Scala DOM Builder – Low-level Scala & Scala.js library for building and manipulating DOM trees
Nikita Gazarov – @raquo
License and Credits
Scala DOM Types is provided under the MIT license.
Comments marked with "MDN" are taken or derived from content created by Mozilla Contributors and are licensed under Creative Commons Attribution-ShareAlike license (CC-BY-SA), v2.5.