Any example of how to use the Dom API?

I see docs for the Dom API but not sure how to use it.

For example, I’m trying to use window.XMLHttpRequest so I began typing Dom.window. and I get no autocomplete for window properties. Does that mean I can’t use dot notation with Dom fields?

2 Likes

I just read in the docs that it only provides types. My bad.

Good question. That’s something we haven’t solved very well in first-party right now. DOM is complex; currently we recommend you bind to the subset you need in your app, with or without the first-party types (doesn’t really matter). It should only be a few lines of bindings.

1 Like

I use bs-webapi, but it can be unwieldy to deal with element hierarchies and the use of pipe-last clashes with the overall direction ReScript is trying to take. If you’re not comfortable writing bindings yourself it’s a good leg up to start with.

2 Likes

Here’s a repository that access the DOM and does drawing on a <canvas>: https://github.com/jdeisenberg/domgraphs

Articles about the program: https://dev.to/jdeisenberg/manipulating-the-dom-with-rescript-3llf

1 Like

@jdeisenberg your code has some stuff I’d like to fix. I might release a post around fixing some tutorials, if I have your permission.

1 Like

Sounds good to me. (Please don’t start with “everything in this is totally wrong” :smiley:)

1 Like

@jdeisenberg I’ve spent some time cleaning up the codebase and putting lots of helpful commit messages at my fork: https://github.com/chenglou/domgraphs/commits/plotGraphs. I’ve stopped before making more UI changes, though I should proceed with those at one point.

These commits reflect the way we’d like our community to work with bindings, and code in general. I highly recommend going over them and understanding our methodologies.

cc @spyder and @jorge (and probably many other community members) who will likely benefit from these commit comments.

4 Likes

Thanks; those are some good points. I think I will go and delete those articles, if you agree that I should do so.

(By the way, I notice that you got rid of a lot of annotations; I like to annotate everything to keep me honest – so that I know what I expect as function input and output.)

Also, the reason I keep parentheses around the parameter of a single-parameter function is for consistency. I have noticed while teaching programmers, especially beginners, they waste neurons on “do I need parentheses here or not?” as opposed to “putting parentheses here always works.”

I like to annotate everything to keep me honest – so that I know what I expect as function input and output.

The annotations aren’t a reliable way to ensure this. For example, during my tuple to record changes, I’ve caught some of your erroneous annotations. Here’s an example: https://rescript-lang.org/try?code=C4TwDgpgBAHlC8UCWA7YAoUkogct6ANhMFAIZ4AUKArgLYBGEATgFywCUCAfFLYyygBqKAEYiJKHRAA5ekzY48ogAzoylaXIHMO6IA

You can feel free to put parentheses anywhere. The formatting cleaned it up in this case.

1 Like

Yes, I see. Do you think I should pull the old articles and rewrite them again?

(Now I remember why I wrote the articles - someone was asking how to use bs-webapi, and I guess the real answer is “don’t.“ :slight_smile:)

No I don’t think you should remove it, and it’s a nice effort that’s appreciated. The people in the future examining this thread will want to know the origin. There are many things you did get right in the codebase; I’ve spent much time refactoring it but I should point out the right stuff too.

But you’re very right on your conclusion, and that is that if we properly start from the product back to the tech, the solutions, paradigms and tools, we end up needing look almost always very different from those we get if we misguidedly started from the tech and tried to shoehorn said tech into a use-case. * We can never truly avoid chasing after shiny things, but we’re a community that has much more to prove vs mainstream languages, so we get much less leeway to start from shiny tech and try to fit those into a use-case.

* Understandably, your goal of the tutorial is to show the language features and libraries, so starting from the tech is slightly more justifiable. But you can probably see how the documenting of that mental model into writing can end up mis-influencing learners to also adopt such approach in production code.

As a matter of fact, I’m aware that my refactoring up until that current commit d7f37d17e835590ca31706a22cf4f407ebba22fc still don’t exemplify the proper approach. My live stream yesterday (which I unfortunately won’t publish because it was long and not high quality enough) explains it a bit. If you give me a bit more time, I’ll prepare a better, recorded live stream on what should actually happen in that app. Tldr spoiler: it turns out that half of my own refactoring goes away once we adjust our perspective of properly crafting the product you’ve shown. The end result is something I believe you’ll be very happy to see. If you feel like rewriting the tutorial after that, then I’m very happy to help. Stay tuned for the stream recording.

3 Likes

Was looking at the commits @chenglou made, and am a little confused at the conclusion made in regards to general type safety. Is the advice to avoid type safety if you can make strong assumptions about the behaviour of your app? It seems like a lot of code written was just plain javascript, making me feel like the value of rescript is nonexistent in those commits

@cvr I’d implore you to read all of my commit messages and comments on there (if only because I’ve spent quite a bit of time making them…)

We’re not avoiding type safety (this also isn’t one of those cases where we’ve loosened on type safety at the external boundaries because of a low ROI). The refactored app is safe, provably so through the types, JS output differences, and yes, like you said, the strong assumptions of the behaviors of that app. We leverage those (as we always should) in order to better type various values, to reflect what they actually are.

If a value is an string, typing it as option<string> doesn’t give you more type safety. In fact, it does the opposite by lying about the number of possible states of that value (how come there’s an extra state now?). If your question is "yeah but what if at writing time I accidentally passed the wrong dom id and now it’s legit None", realize that typing it as option<string> still doesn’t help you catch it at compile time. As a matter of fact, you’ve just accidentally handled it at runtime, and potentially silenced that critical mistake, ironically by making the None state a legal state of your app. Obviously, the solution is not to silence it with a None branch, but to fix that at writing time.

It seems like a lot of code written was just plain javascript, making me feel like the value of rescript is nonexistent in those commits.

See my previous post and my previous paragraph. We should never start from “where can I make ReScript’s particular features shine” and be disappointed when they don’t fit into a use-case. That’s backward. Taking a step back, you might realize that, contrary to perhaps your own desire, you’ve accidentally justified that the code looking like JavaScript is a bad thing; it’s a great, unique value proposition! Not to mention I refactored the code into that while preserving type safety, fast compilation and other benefits. You see how starting from the shiny tech can mislead one’s own goals?

But even beside that point, it’s plain untrue that the value of ReScript is nonexistent in those commits. I refactored the code; ReScript absolutely helped during refactoring in all the typical ways we know it’d have helped. I’d invite you to try to refactor that yourself starting from the first commit and experience the value yourself.

(By the way, the original code actually had unsafe conversions, one of which I’ve removed in e5f725746aab03f910110bb8d4efdd2bcd6d4ac0 precisely thanks to the above explanations).

3 Likes

@chenglou I enjoyed reading all of the commit messages you made and the refactoring you’ve done. I’m extremely interested in rescript and using it language of choice. I recently refactored my app to use reasonml (only due to better editor support). I’m still quite new, and the only typed language experience I have extensively is typescript. As I’m soon to start a side project and want to use rescript, I find what you did extremely useful.

I should rather say for the purpose of discussion, what would you say are the advantages/disadvantages of using rescript in this particular case over typescript?

1 Like

Glad you like them! Generally speaking our value props are documented at https://rescript-lang.org/docs/manual/latest/introduction already.

In this particular case, it’s precisely what I’ve pointed out in my previous message. Consider that we’ve successfully reduced option<string> to string (that’s just an example. The actual commits in the codebase reduced spiritually similar things). In ReScript, you know when it’s a string, it’s never option<string>. Thanks to the strong assumptions of the app as you’ve pointed out, and a type system that doesn’t lie, you can sleep well knowing there’s actually no None case that you ever need to handle.

Compared this to TypeScript, where you equally try to leverage the strong assumptions of your app, and type something as string instead of undefined | string. TS’ type system being unsound means looking at a string doesn’t actually guarantee that you’ll never get undefined. So your conclusion of "there’s truly no undefined" will be shaky for various reasons.

Altogether, consider the cases we’ve discussed, and their impact on reasoning during debugging, when you e.g. pass a wrong DOM node ID during querying and produce a None:

  1. You use ReScript and type the DOM value as option<string>. Oops, that illegal state got accidentally handled by some None branch(es) somewhere. You now get a runtime message regarding some null value. But you don’t know whether this is a proper handling of a truly null value (e.g. something came from the network payload), or because you’ve made a typo in the node ID, or whatever else. You end up debugging though all the callsites that wrongly pretended that they might need to handle None. Monadic abstractions make this even worse because you’re obliged to lift all the callsites.
  2. You use ReScript and type the DOM value as string (after verifying all the invariant we’ve said above). You still get a runtime null, but because you know though the assumptions and the type that it should have never been null, you can immediately rule out mishandled branches and other possibilities, and straight up conclude that your DOM querying was incorrect. You quickly fix this at writing time and resume maintaining the assumptions.
  3. You use TypeScript and type the DOM value as string. If you get a null value, you need to debug it the same way as in case 1.
  4. You use TypeScript and type the DOM value as undefined | string. That doesn’t help much.

(Hopefully you can see how starting from wanting to use variants here would often mislead you into case 1 while proudly thinking that you’ve leveraged some shiny language feature and made your development experience better, blissfully unaware that you’ve made your coding life harder and wondering why others aren’t as receptive of your language proposition. This is a big downside of any shiny tech that we try hard to avoid.)

Hope that makes sense?

2 Likes

Ah, I see. That makes sense, thank you!

1 Like

Hi,
Cheng’s opinion is his personal opinion, I would say his suggestion is valid most of the time, but it does not mean we will enforce a single style as a community.
Your previous work is very much appreciated, people can learn something from it even if it’s not 100% correct sometimes.
As a community, we are inclusive since people may have different tech background.

6 Likes

Although I like the simplicity of the result (I was never a fan of bs-webapi’s ergonomics myself), I feel like this refactor has some shortcomings if it is to demonstrate how to approach problems such as interaction with DOM APIs. Hoping that it is useful feedback for the team, I’m going to take this refactor at face value and give my two cents.

Take this commit for example: https://github.com/chenglou/domgraphs/commit/0727144626b7a6013d297b6aab74d056cb07f889#diff-6b86dad133536de8149349ea05fd8585R51

I think this only works when you’re refactoring working software to make the code simpler. It benefits from:

  • Having some code that was correct and performed the task well in the first place
  • Being able to look at and compare the generated JS output of some existing code
  • Having a good command of DOM API’s already before attempting to write this code from scratch

It is indeed easier to read and follow in places (due to bs-webapi’s API design and difficulty of making generic DOM bindings), but I’m not sure if it is easier to write, let alone easier to write correctly. Concerns such as false sense of safety or inaccuracies regarding nullability are important things to address of course. But knowing how often I get my hand-written bindings wrong, how hard they are to maintain when the underlying JS library keeps changing, and how many head-scratching bugs they caused me in the past, I don’t want to be in the mindset of “always interacting with JS” like when writing code such as context["moveTo"](~x=0.0, ~y=centerY). Thankfully DOM API is more stable than rest of the JS ecosystem and very well documented, but I would also be afraid to have this sort of arbitrary JS access all around a big codebase only to have the underlying library change its API making it a really error-prone upgrade process (instead of updating the bindings and letting the type system help with the rest of the work).

When building something such as domgraphs I heavily rely on tooling. With JavaScript and TypeScript you get a lot of help from editors such as VS Code. It allows you to discover APIs and get your code right (although it is sometimes unsound). libraries such as bs-webapi or Fable.Browser also provide a similar boost when interacting APIs as complex as DOM. Of course it is not a replacement for having a good working knowledge of the underlying API, but I would always want more assistance when writing code than having to have MDN or some JS libraries README always open on the side.

Another way of looking at it; Old browsers or different execution environments not having some features doesn’t invalidate the entirety of https://github.com/microsoft/TypeScript/blob/master/lib/lib.dom.d.ts, and I’m glad there’s a peer reviewed collection of types for DOM used by many people and written by people who have much more experience than I do.

For example, with let width = Belt.Float.fromInt(element["width"]) I definitely see myself omitting Belt.Float.fromInt if I don’t know the return type by heart. Or whether it can be null or not. In JS there’s already a culture of overly defensive code such as typeof element.width = "number" ? ... : .... People don’t start out writing code this way, but after one too many easily avoidable errors on Sentry, they adopt this pattern to interact with any and every function someone else wrote.

Re-creating all of this ecosystem of bindings with a small community and gargantuan API’s such as DOM that don’t always align with ReScript’s type system is an uphill battle indeed. Maybe ReScript tooling (not necessarily the compiler, but definitely the editor) can help out with this if it can seamlessly utilize TypeScript types to display hints when interacting with JS without bindings. Some editor features that would help with the following line
context["fillRect"](~x=0.0, ~y=0.0, ~w=width, ~h=height) :

  • I would like to have fillRect suggested as I type
    Personally, it is always a pain if I need to stop and google this sort of stuff. I might not know what this method is called, or how to spell it, or whether it exists at all. Autocomplete is a massive productivity boost.

  • I would like to see these labelled arguments suggested as I pass arguments.
    I really have no idea what order I need to pass them. Using labelled arguments help as documentation. But I first need to figure out what each of these positional arguments in one specific overload of the original JS function stand for and the accepted types.

    I also don’t know if I’m passing an unused argument to this function, or whether I need to (or can) pass more arguments.

    Slightly off topic; using labelled arguments in this case makes me think that I can pass them in a different order or I can insert another argument in the future at an arbitrary position, which is not the case.

  • I would like to be able to jump to definition and read signatures, docs. The destination could be lib.dom.d.ts#L3397

Of course this doesn’t help with poorly documented JS libraries much. There’s too many popular libraries with a single README.md acting as documentation containing things like “drawUnicorn: takes a canvas and draws a unicorn according to the options passed as the second argument” and having no other information. At least with bindings someone figures this stuff out once which helps everyone else. Unfortunately I also often don’t have the luxury of being able to say “I won’t use this poorly written library” or “All of the code we interact with in our project is bespoke apart from platform APIs”. I can’t count the number of times I was able to find someones hand-written Flow or TypeScript documentation for a JS library on Github without which I was unable to figure out how to use the library correctly in JS. I don’t think “with ReScript you don’t need bindings” tagline really clicks with me.

10 Likes

I’m quite new to ReScript, but more experienced with TS comparably. I can’t clearly understand what you’ve mentioned here. If I type something as string in TS, what makes TS can’t actually guarantee that it will never get undefined? How does ReScript achieve it over TS?

2 Likes