Using Gleam in HTML with as little JavaScript knowledge as possible
I've been looking at the Gleam language recently. Among other features, it can be compiled to JavaScript, and thus presumably used on web frontend. I wanted to try that. This is a short tutorial on how to do that with little idea how JavaScript is supposed to work.
My initial JS knowledge: alert(3), onclick and some basic selectors, i.e. the little stuff that is useful to add minor interactivity to HTML pages and implement trivial ViolentMonkey scripts. Namely: I have no knowledge about modules and whatnot, and this post is just a result of my trial-and-error attempt at embedding my Gleam. I succeeded, but still have no idea what I am doing :-)
My code
Let's start with some trivial code, in src/lol.gleam (in a project initialised with gleam new lol and the repeatedly package added with gleam add repeatedly):
import gleam/io
import repeatedly
pub fn say_hello() {
repeatedly.call(2000, Nil, fn(_state, _call) {io.println(get_greeting())})
}
pub fn get_greeting() {
"Hello World!"
}
pub fn main() {
io.println("3, 2, 1, go!")
}
This snippet has many of the basic stuff I might need in future: it does some io, it uses another package , it returns data I will want to show. At first I am mostly interested in multiplatform stuff (i.e. running also on the BEAM backend), so I don't want to use any DOM frameworks at first, though I will mention some of the ways later in the post.
So, build this for JavaScript: gleam build --target javascript, and stuff happens. It is not very clear what to do now, but grep``ping through the ``build/ directory shows that the built code lives in build/dev/javascript/lol/lol.mjs.
Embedding the code
Here comes the fun part: the code is in JavaScript module, not plain code. That comes with several surprises:
- It can only be imported from other modules, not by “plain” code in global scope
- Due to CORS, imports only work with network schemes, not with the file:// one
- Due to scoping, I have not found a way of using the functions from devtools console
Big wins for the platform /s, but we have to live with that, so let's try to write the respective HTML for this. I put that in main.html in the project root, but it does not probably matter. The code:
<!doctype html>
<html>
<head>
<meta charset=utf-8> <!-- firefox complains otherwise -->
<title>Buh</title>
</head>
<body>
<!-- some random elements to work with -->
<button id=butt>Klik mee</button>
<p id=uwu>
<!-- the “binding” to our Gleam code. This probably needs to be at the end of the page, since it needs to be able to use the selectors. -->
<script type=module> //It has to be a module to allow imports
import * as lol from "./build/dev/javascript/lol/lol.mjs";
// Set the text of one element to the computed stuff:
let par = document.getElementById('uwu');
par.innerText = lol.get_greeting();
// Let's have an interactive button (I love the come-from pattern, but using `onclick` would be even trickier…)
let butt = document.getElementById('butt');
butt.addEventListener('click', lol.say_hello);
// Apparently, the `main` function does not get run automatically, so call that explicitly.
lol.main();
</script>
</body>
</html>
Shoutout to the person who asked the same question on StackOverflow. From this point on, we only need a HTTP server; luckily, Python has one in stdlib, so just calling python3 -m http.server 12312 in the project root lets us load http://localhost:12312/main.html and see our page.
Next steps, alternatives et cetera
When developing web frontend in Gleam, the more common way is using a framework like Lustre or at least a DOM library (there are several, I have no idea how mature those are). I did not go this route yet, because I am more interested in using backend-agnostic Gleam code from JavaScript and don't mind writing the trivial bindings in JavaScript.
Also, a common thing to do is using a minifier+bundler like esgleam (it uses esbuild under the hood), so that the whole project is in one file. I don't think I need that now (having the JS be readable is more important to me atm and I don't want to complicate things further), but there is at least one tutorial on how to do that.
Also, during writing of this article I realised Gleam can run all the JS code from itself, so I could have a js_main function that would bind the HTML from Gleam itself. But this is probably more readable and separated anyway.