-
-
Notifications
You must be signed in to change notification settings - Fork 28
Description
Currently, the design of SDT type listings is maximally generic, providing a lot of flexibility to esoterically minded library authors.
The issues below mostly apply to all types of keys – props, attrs, css style props, and event props, but we'll be looking at specific event props as a concrete example:
trait MouseEventProps[
EP[_ <: DomEvent],
DomEvent,
DomMouseEvent <: DomEvent,
DomElementMouseEvent <: DomMouseEvent,
DomDragEvent <: DomMouseEvent,
DomWheelEvent <: DomMouseEvent
] { this: EventPropBuilder[EP, DomEvent] =>
/**
* The click event is raised when the user clicks on an element. The click
* event will occur after the mousedown and mouseup events.
*
* MDN
*/
lazy val onClick: EP[DomElementMouseEvent] = eventProp("click")
// ... more props here
/**
* Fires when the mouse wheel rolls up or down over an element
*/
lazy val onWheel: EP[DomWheelEvent] = eventProp("wheel")
// ... more props here
}
A developer who uses a UI library like Laminar (or Outwatch or Calico) might see onWheel --> whatever
in their code, and ctrl-click on the onWheel
symbol in their IDE to go to its definition. The snippet above is where they will land.
The definition of onWheel
has a short ScalaDoc comment, but there is no concrete information about the value we're dealing with. We don't know what this EP
type is, or what shape DomWheelEvent
is – these types are both abstract.
We could improve on DomWheelEvent
by replacing it with org.scalajs.dom.WheelEvent
– the concrete type that it resolves to for 99% of the users. Then users could proceed to ctrl-click into that type and see what kind of events they will get. This would improve ergonomics for most people. This benefit is obvious, the main issue is maintaining functionality for the other 1%, the use cases where we do want abstract type params.
Currently DomWheelEvent
is an abstract type param because our trait MouseEventProps
is in a shared
project – it's cross-compiled for Scala.js and the JVM, and so it does not have access to the JS-only org.scalajs.dom
types.
I designed it that way because I was hoping that Scala DOM Types would be used on the JVM as well, for example in tools that need to render HTML (web pages or email content) on the server. To my knowledge, as of today no such tools exist that use Scala DOM Types, but it's possible that they do exist, just that their authors are overly modest, and didn't let us know.
I also hoped that cross-compiling libraries would be using Scala DOM Types on both client and server, however, again, I'm not aware of any such libraries.
Also, @armanbilge pointed out that obviously there aren't enough IO monads in scalajs-dom, so he's looking into using our generic SDT design with a more FP-friendly alternative to scalajs-dom.
How could we provide concrete org.scalajs.dom types to the 99% of SDT users, while maintaining SDT's flexibility – one of our main design goals? I don't want developers to need to fork SDT or copy-paste our type listings into their project. Ideally there should be a single source of truth for SDT-style type listings in the Scala world. And thus it needs to be flexible enough for anyone to use.
I think one way or another, the answer has to involve code generation.
-
We could keep existing type listings as-is in the
shared
project, with all their abstract params, and generate ScalaJS-specific copies of all those traits. A bunch of dumb stuff like replacingDomWheelEvent
withdom.WheelEvent
. Or, do it the other way, doesn't really matter. Laminar would use the ScalaJS version, other libraries which need more flexibility would use theshared
project version with abstract type params. If someone is using both libraries in their ScalaJS project, redundant near-copies of SDT traits will be loaded into the JS bundle. -
Alternatively, we could keep some kind of "source of truth" for the type listings in the SDT project (whether in the shape of Scala traits, or something more structured and easier to parse), and have every consuming library (Laminar, Outwatch, etc.) generate their own bundle of DOM keys. We would need to provide a configurable generator for this, and I'm not quite sure how. Otherwise, same issues as option (1) in case you use more than one such library in your project, but there are some extra advantages too:
a) In addition to making
DomWheelEvent
a concrete type, we could also makeEP
a concrete type, so in Laminar you would see a fully concrete type:lazy val onWheel: EventProp[dom.WheelEvent] = eventProp("wheel")
b) Each library like Laminar could better customize what they want to generate, e.g.
click
vsonClick
,typ
vs`type`
, etc. (this can be seen as a disadvantage too).c) For special types like
DomElementMouseEvent
(canonicallyTypedTargetMouseEvent
), libraries would be able to choose the strategy they want, AND have that type be concrete too.d) Just like today, libraries could choose which traits they want to use, but the generator could also flatten the traits into a single fat trait, or into one trait per key type (props / attrs / etc.). Currently libraries need to create an
object
that mixes in 20+ traits. I have no specific insights, but I suspect that there are ways in which this is unhealthy in JS / ScalaJS.e) Libraries would be able to choose if they want
val
-s orlazy val
-s (See Switch definitions from lazy vals to vals? #86)f) All the DOM types would be contained in the library project itself, no need to jump into SDT.
g) Esoteric libraries that need something other than scalajs-dom would get the benefit of concrete types as well.
-
We could "backport" some of the advantages of option (2) to option (1) if we generated a set of "blessed" SDT configurations inside SDT, but I'm not sure if that's a good tradeoff, e.g.
EP
type would still remain abstract.
Disadvantages of code generation:
-
The inelegance of redundant code, whether contained in SDT, or spread across several projects. There is no escape.
-
Working with generated code can sometimes feel like working with macros, i.e. "invisible code". We will avoid this by committing all generated code into github, so that all published code matches what you see on Maven Central, in the IDE, and on github. We do it this way in Airstream already.
-
Harder for users to figure out how to contribute
Examples of code generation:
-
Tyrian: https://github.com/PurpleKingdomGames/tyrian/blob/2501309271cd9cb07af4a63d616d0eec0b1d3a2e/project/TagGen.scala (cc @davesmith00000 in case this SDT issue is of interest to you)
-
Airstream (Not DOM related): https://github.com/raquo/Airstream/blob/master/project/GenerateCombinableEventStream.scala
Requesting comments!
I'm not yet sure which option is best, or whether the tradeoffs are worth it. There might be better options than what I've considered, or some other use cases I haven't realized. Any opinions or insights are welcome. Let me know what yall think.
Personal cc: @sjrd @keynmol @armanbilge @fdietze @cornerman @yurique @sherpal but the discussion is open to everyone.