Accessing and tweaking Dom elements (maybe related to type conversion?)

Hi, I’m trying to convert myself from Ts/Js to Rescript just because I want to see the appeal of it.

I hope to get some help on what is the right way to do this, as an example:

  • select all elements of class “abc”
  • print their zIndex
  • change their zIndex to 200
@scope("document") external querySelectorAll: string => Dom.nodeList = "querySelectorAll"
@get external style: Dom.htmlElement => Dom.cssStyleDeclaration = "style"

// do I have to do this ugly conversion always?
external toHTMLElement: Dom.node => Dom.htmlElement = "%identity"
external cssStyleToRecord: Dom.cssStyleDeclaration => {..} = "%identity"

@send external forEach: (Dom.nodeList, Dom.node => unit) => unit = "forEach"
let stylize = () => {
  ignore
}
let trace: 'a. ('a, string) => 'a = (value, msg) => {
  Console.log(msg)
  Console.log(value)
  value
}

let main: unit => unit = () => {
  let elements = querySelectorAll(".abc")
  let getStyleRecord = x => x->toHTMLElement->style->cssStyleToRecord
  elements->forEach(el => {
    el
    ->getStyleRecord
    ->trace("converted record ")
    ->Object.get("zIndex") //is there any other way to access the field from a Dom.cssStyleDeclaration, without converting?
    ->Console.log
    el
    ->getStyleRecord
    ->Object.set("zIndex", 1)
  })
}
main()

How do I do what did in a more elegant way?
(preferably with explicit type because I’m trying to learn ReScript and the type inference can get in the way of understanding how it handles data/type)

Thank you!!

The DOM types that ship with the compiler are pretty bare bones, but here’s a couple of suggestions to make it a bit easier:

  1. You can use JsxDOMStyle.t instead of Dom.cssStyleDeclaration to get a fully typed style object.
  2. You could just assume nodeList contains only htmlElements in your forEach binding, or if you want more flexibility, add a generic nodeListOf type, similar to what TypeScript has.

Here’s the equivalent piece of code implementing those two suggestions:

type nodeListOf<'a>
@send external forEach: (nodeListOf<'a>, 'a => unit) => unit = "forEach"

@scope("document") external querySelectorAll: string => nodeListOf<Dom.htmlElement> = "querySelectorAll"
@get external style: Dom.htmlElement => JsxDOMStyle.t = "style"

let trace: 'a. ('a, string) => 'a = (value, msg) => {
  Console.log(msg)
  Console.log(value)
  value
}

let main: unit => unit = () => {
  let elements = querySelectorAll(".abc")
  elements->forEach(el => {
    let style = el->style->trace("converted record ")
    style.zIndex->Console.log
    let style = {...style, zIndex: "1"}
  })
}
main()

Also, on a more technical point, {..} is an object type, not a record type. And your cssStyleToRecord function is not soundly typed, because the returned object type will be inferred from use, that is, it will be assumed to have whichever fields you use from it. You should generally avoid object types. It’s more of a historical artifact.

  1. I’m abit confuse about type nodeListOf<'a>. What does it mean when you are not giving thing to that type?
  2. How do you know about JsxDOMStyle? (From the javascript thingy I expected the style to be cssStyleDeclaration)
  3. The JsxDOMStyle zIndex is not mutable. I guess I would have to make my own similar Record type with mutable?
  4. I see. I tried reading the doc a bit more and found out about @set_index

Thank you for your advice!! I changed the script to be like this. Is there anything else I should change?

type nodeListOf<'a>
type made_up_style_type_to_test = {mutable zIndex: int}
@scope("document") external querySelectorAll: string => nodeListOf<Dom.element> = "querySelectorAll"
@get external style: Dom.htmlElement => made_up_style_type_to_test = "style"

// do I have to do this ugly conversion always?
external toHTMLElement: Dom.element => Dom.htmlElement = "%identity"
@get_index external get: ('b, string) => 'a = ""
@set_index external set: ('b, string, 'a) => unit = ""

@send external forEach: (nodeListOf<'a>, 'a => unit) => unit = "forEach"

let main: unit => unit = () => {
  let elements = querySelectorAll(".abc")
  elements->forEach(el => {
    el
    ->toHTMLElement
    ->style
    ->get("zIndex")
    ->Console.log
    el
    ->toHTMLElement
    ->style
    ->set("zIndex", 222) //this works with @set_index

    (
      el
      ->toHTMLElement
      ->style
    ).zIndex = 1 //I try to change the property with my homemade css with mutable zindex. this also works!!
  })
}
main()

Not sure what you mean here. That you’re not giving it a definition? That means it’s an abstract type that you can’t interact with directly. You have to write bindings to operate on it.

JsxDOMStyle is used in the rescript-react bindings, and the much more common way of interacting with the DOM. Most people will very rarely interact with the DOM directly, which is why the types are so bare bones.

Mutable state is discouraged. Much easier to reason about your code if you avoid mutation. Do you really need it?

The get function is unsound, in two ways. 1) The return type isn’t restricted by anything, and so will be inferred entirely based on how it’s used later. And 2) The return type should be an option, since the property you’re requesting might not be set. Although because of 1) it will be inferred to be an option if that’s how you use it. But it’s highly error-prone. And unnecessary when you have a type where you can access the field directly.

1 Like

Thank you for your respond!!

Not sure what you mean here. That you’re not giving it a definition? That means it’s an abstract type that you can’t interact with indirectly. You have to write bindings to operate on it.

Yeah I mean no definition. I see, I understand that now.

JsxDOMStyle is used in the rescript-react bindings, and the much more common way of interacting with the DOM. Most people will very rarely interact with the DOM directly, which is why the types are so bare bones.

Make sense I see!

Mutable state is discouraged. Much easier to reason about your code if you avoid mutation. Do you really need it?

For this example, yes. I need to change the zIndex of the element so that it’s placed behind other things.

  1. The return type isn’t restricted by anything, and so will be inferred entirely based on how it’s used later.

I see. It’s the nature of @get_index / @set_index, right?
Because they seem to just access the raw external property from Javascript so they can’t really check for type.

Even if I define it as get: ('a, string)=>option<string> and I by mistake use it to get some weird property like get(someHtmlElement, "classList") I would get some weird DOMTokenList that ReScript thinks to be option<string>.

It doesn’t matter even if I make multiple @get_index and @set_index for different types right?

@get_index external getStrProp : ('a, string) => option<string> = ""
@get_index external getFloatProp : ('a, string) => option<float> = ""
// ....

It’s more so that the type notation is there so that you don’t pass its returned value to a wrong function later on OHH WAIT… I GET IT, IT’S ALWAYS ABOUT that. It’s about not using those returned value wrongly. The part where we get those value from JS is the unsafe part that we have to handle carefully to make sure rescript gets the right data type info.

And unnecessary when you have a type where you can access the field directly.

I wasn’t sure which way is more standard/safe to do things in rescript.
It seems in general I see the following ways:

For property:

  • use @get, @set tag to bind the property directly @get external style....="style"
  • use @get_index, @set_index to dynamically get property (with a rather unsound type problem)
  • make a type record with accessible field

For method

  • use @send
  • make a type record with a property as the function type

ReScript is fun, though the puzzle is at this point for me more about binding things the right ways than actually solving programming problems.

Just to make sure I’m doing it right: we are supposed to write the binding ourselves right? We don’t have a binding presets of common javascript functions as of now right?

1 Like

It’s safe to use with dicts, where all the keys will gave the same type. Otherwise, not so much, no. You can also constrain the allowed property names by using polymorphic variants, and have multiple differently typed functions that way:

@get_index external getStrProp : ('a, [#foo | #bar]) => option<string> = ""
@get_index external getFloatProp : ('a, [#x | #y]) => option<float> = ""

Yes, if you get the boundaries typed correctly, the compiler will help you avoid errors that can otherwise be very hard to track down. That principle doesn’t just apply to JS interop, but also the boundary between client and server, making sure the data you receive from the server is actually what you expect.

For methods you should almost always use @send. A record with function types can cause problems binding to this.

There are some ready-made bindings in rescript-webapi. But since it’s a big API that isn’t very well-suited for static typing, rescript has evolved quite a bit since this was initially designed, and direct DOM manipulation is quite rare in rescript, it’s usually better to just bind to it yourself. You can copy and paste from these bindings to get something up quickly though, and then just adapt to your needs.

2 Likes