Gleam in the Browser

Mar 28, 2023   Kero van Gelder

This post is part of a series of four, Gleam calling Erlang and v.v., A Rebar3 project using Gleam, Gleam calling JavaScript and v.v. post, and this post.

In this post, we look at running Gleam in the browser. This comprises setting up an HTML file, loading our Gleam code (compiled to JavaScript) and starting it.

Happy, again

We revisit our happy project from the earlier posts.

We can run the generated JavaScript from HTML as follows, in ./index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
    <script type="module">
        import { main } from "./build/dev/javascript/happy/happy.mjs";

        main()
    </script>
</body>
</html>

We can conveniently import the generated JavaScript, since the gleam compiler outputs modules. All that remains is calling the main() function, i.e. what gleam run was doing before.

If you load this HTML file as file:///path/to/happy/index.html in a permissive browser, you will see the same output as before, but now on the Development Console (hit F12 and select the Console tab). Unfortunately, recent browsers are not permissive.

A local Web Server

Best to go to a local web server, directly. If you have experience with Erlang servers, you could launch your favourite one.

On the other hand, we are going to use libraries from the JavaScript eco-system, later on, so why not pick a modern JavaScript server?

Use your package manager to install Node JS, you may have it around from the previous post. This time, we are interested in the package manager, npm. Run:

npm i --save-dev vite
npx vite serve

It will serve ./index.html, and the whole directory tree. Browse to the URL that vite gives you, and you should see the promised output on the Development Console.

Vite can do a lot more:

[Update Nov 19] when you install the vite-gleam plugin with npm i --save-dev vite-gleam and create the file ./vite.config.js that contains:

import gleam from "vite-gleam";

export default {
  plugins: [gleam()],
};
you do not have to manually recompile gleam anymore, which creates an even smoother development cycle.

See something on the screen

The console is good for development and debugging, but not for end-users. We need Gleam to manipulate the Document Object Model (DOM) of the HTML page. I am going to use my favourite library, lustre, because it is an implementation of The Elm Architecture for Gleam.

gleam add lustre

Replace ./src/happy.gleam with

import lustre
import lustre/element.{text}
import lustre/element/html.{h1}

pub fn main() {
  lustre.element(h1([], [text("We are Happy!")]))
  |> lustre.start("#root", Nil)
}

This is a very basic lustre application, that effectively omits Model, and Update. It does not get any simpler; but this post is not about explaining lustre. Don't forget to compile our happy project!

This lustre application is placed within the DOM, inside the element matching the CSS path "#root". We must add such an element to our HTML file. Add

<div id="root"/>
to index.html, just before the <script> When you go to your browser, Vite probably already reloaded the page, and you can see that we are happy!

! If you are using your favourite Erlang web server, at some point you are going to run into JS import problems: when you include an npx package, it will reside in ./node_modules, but (generated) .mjs files (a) will encounter incompatible (non-mjs) files, (b) may not know the relative path. That is because Node is using its own mechanism for including packages. There are various JavaScript packages that are aware of both the browser modules and node's system, such as Vite and webpack. For Vite, run npx vite build and then you can serve ./dist/ statically with any web server. Historical note: JavaScript had no import system when it was launched. IMHO, the result is sub-optimal, and it is best to let Vite handle it.