raquo / scala-dom-types

Scala types for your library to represent HTML tags, attributes, properties and CSS styles
MIT License
91 stars 27 forks source link

Add info about supported tags to props/attrs #96

Open armanbilge opened 1 year ago

armanbilge commented 1 year ago

For example, the current data for the label attribute is:

https://github.com/raquo/scala-dom-types/blob/6900584aea4e3ae85c5349e4e57ada925a849d4b/shared/src/main/scala/com/raquo/domtypes/defs/reflectedAttrs/ReflectedHtmlAttrDefs.scala#L609-L622

At least according to this page, it is supported only for <track>, <option>, <optgroup>, <menu>, and <menuitem> tags.

https://www.geeksforgeeks.org/html-label-attribute/

In fact, I'm confused—is the label for <track> and <option> really the same?

In any case, Calico would be able to take advantage of this information.


The reason I'm thinking about this now, is because I am looking at web components, and they often seem to be defining their own custom attributes that may be "clashing" with other HTML attributes.

For example, I'm stuck on label specifically because of its use in Material Button. https://github.com/material-components/material-web/blob/e15c4b86d584cfda5dc850cb697bc9b9552e9536/docs/quick-start.md#usage

Basically we seem to have a "namespacing" issue, where "attributes" should be "namespaced" within their element, because they may have different implementations and types across elements (or not even be available).

raquo commented 1 year ago

Before you go down this road, keep in mind that an attribute's applicability is not just based on the element type, it can require all kinds of complex conditions that are very hard to express in types, e.g. see the example here: https://github.com/raquo/scala-dom-types#reasonably-precise-types

Also, I'm not sure how knowing which element types an attribute applies to will help you de-conflict two label attributes.


In fact, I'm confused—is the label for and

It has the same name and expects the same value type (string), they serve approximately the same purpose, that's about as "same" as it gets in the soup that is DOM. The native JS call to set the label attribute is the same in both cases.


If this is just for web components, then you can define special attribute types specific to that web component, without complicating the types of regular HTML attributes.

In Laminar the canonical approach is to scope the web component's attributes to the web component itself, and access them as properties, e.g. like so:

Button(
      _.id := "myButton",
      _.label <-- actionVar.signal,
      ...
)

Which is just syntax sugar for:

Button(
      Button.id := "myButton",
      Button.label <-- actionVar.signal,
      ...
)

(from https://laminar.dev/examples/web-components, but Antoine's Laminar SAP UI5 Bindings takes the same approach)

This syntax allows using both specific webcomponent attrs as shown above, as well as regular html properties (e.g. (_ => idAttr := "someId")) that you might not want to redundantly define on every web component.

armanbilge commented 1 year ago

Thanks!


Before you go down this road, keep in mind that an attribute's applicability is not just based on the element type, it can require all kinds of complex conditions that are very hard to express in types

Sure :) I did re-read that "reasonably precise" bit before opening this issue. FWIW I'm not on any particular mission to make the API more typed, I think what we have is good. It just happens that the way Calico is currently working, we can do this "for free". And also, the web component thing put this issues of clashes onto my mind.


If this is just for web components, then you can define special attribute types specific to that web component, without complicating the types of regular HTML attributes.

Yep, I studied the approach in the UI5 bindings. I'm not sure if this technique will work with how we are currently handling "modifiers" in Calico. And/or I didn't like it for other reasons 😂

raquo commented 1 year ago

Also just for the record an older issue about this is https://github.com/raquo/scala-dom-types/issues/13 but that was waaay before we had generators.

Now that we have generators, different UI libraries can pick and choose how much of the available data they want to use, so in principle I don't mind if someone added this data to SDT.

armanbilge commented 1 year ago

Also just for the record an older issue about this is #13 but that was waaay before we had generators.

Ah, my bad, I couldn't tell from the title :)


Also, I'm not sure how knowing which element types an attribute applies to will help you de-conflict two label attributes.

Good question. Here's a prototype of how I plan to do this in Calico. In particular, check out the fizzBuzzAttr which is a Boolean for fooTag and an Int for barTag.

//> using lib "org.typelevel::shapeless3-deriving:3.3.0"
//> using option "-Ykind-projector:underscores"
import shapeless3.deriving.K0

trait Modifier[E, M]

trait SetAttribute[A, V]

class Tag[E]:
  def apply[M <: Tuple](modifiers: M)(using
      K0.ProductInstances[Modifier[E, _], M]
  ): E = ???

class Attribute[A]:
  def :=[V](value: V): SetAttribute[A, V] = ???

// common to both
def sharedAttr: Attribute["shared"] = ???

// only on foo
def fooAttr: Attribute["foo"] = ???

// only on bar
def barAttr: Attribute["bar"] = ???

// on foo and bar, but with different types
def fizzBuzzAttr: Attribute["fizzbuzz"] = ???

trait Element
given sharedForElement[E <: Element]: Modifier[E, SetAttribute["shared", String]] = ???

trait FooElement extends Element
given fooForFooElement[E <: FooElement]: Modifier[E, SetAttribute["foo", String]] = ???
given fizzBuzzForFooElement[E <: FooElement]: Modifier[E, SetAttribute["fizzbuzz", Boolean]] = ???

trait BarElement extends Element
given barForBarElement[E <: BarElement]: Modifier[E, SetAttribute["bar", String]] = ???
given fizzBuzzForBarlement[E <: BarElement]: Modifier[E, SetAttribute["fizzbuzz", Int]] = ???

def fooTag: Tag[FooElement] = ???
def barTag: Tag[BarElement] = ???

def demo =
  fooTag(
    sharedAttr := "hello",
    fooAttr := "foo",
    fizzBuzzAttr := true
  )

  barTag(
    sharedAttr := "hello",
    barAttr := "bar",
    fizzBuzzAttr := 42
  )
raquo commented 1 year ago

Ah, I see. I figured it'd be something with implicits. Interesting pattern.

Note: I don't think that there's ever a case in the DOM where the same attribute / property has a different type based the element type. Like, the label attribute will always be string, for all elements that it's defined for. The only part I'm not sure about are such incompatibilities between HTML and SVG attribute names.

armanbilge commented 1 year ago

Ah, I see. I figured it'd be something with implicits. Interesting pattern.

Part of my motivation for this pattern was to avoid relying on implicit conversions, which seem to be discouraged in Scala 3.


Note: I don't think that there's ever a case in the DOM where the same attribute / property has a different type based the element type.

That may be true, but IIUC that seems down to luck or "good practice" rather than a hard rule.

The only part I'm not sure about are such incompatibilities between HTML and SVG attribute names.

Exactly, and web components creates new potential for such incompatibilities. As you point out, attributes for web components could be handled differently. But one of the big ideas of web components in HTML is that they can be used like vanilla elements, so I'm interested to see if we can preserve that experience.