Hi, I’m Erika Rowland (a.k.a. erikareads). Hi, I’m Erika. I’m an Ops-shaped Software Engineer, Toolmaker, and Resilience Engineering fan. I like Elixir and Gleam, Reading, and Design. She/Her. Constellation Webring Published on Modified on

Embedding Gleam in my blog with Vite

My interest in Gleam came primarily from the idea of having Erlang with Static types. Gleam is able to transpile to Javascript, so I wanted to try embedding it in my blog. I might want to add interactive explanations to my blog in the future, and it would be neat to be able to write them in Gleam.Update 2024-01-06: I now recommend using esgleam instead of vite-gleam, which I’ve written about here. All the Gleam explanation still lives here, but the simplified deployment instructions are in the new blog post.

Step 1: Create Lustre Project

Lustre is a frontend web framework for Gleam. It has an Elm-inspired runtime that makes it easy to create dynamic components with Gleam.Lustre can also be used on the server side as a HTML templating DSL.

I can create a new Gleam project with the CLI:

gleam new shiny_counter

I can then enter the newly created project folder and add Lustre:

cd shiny_counter
gleam add lustre

Now I can start building my Lustre application.

A simple Lustre application consists of an initialization function, an update function, and a view function.

Init

The initialization function is responsible for setting the initial state of the Lustre model.

In my shiny_counter it looks like this:

type Model =
  Int

fn init(_) -> Model {
  0
}

Here, I define the Model to be just an integer, and initialize that integer to 0.

Update

The update function takes a model and a message as arguments and returns an updated model based on the contents of that message.

In the case of shiny_counter I only need an increment and decrement message:Since the model of shiny_counter is an Integer, it’s signed, so if I send a Decrement message enough times, I will get a negative number. If I wanted a model where I couldn’t produce a negative number, I would want to add extra cases into my update function.

pub type Msg {
  Increment
  Decrement
}

fn update(model: Model, msg: Msg) -> Model {
  case msg {
    Increment -> model + 1
    Decrement -> model - 1
  }
}

View

The view function takes a model and returns a Lustre Element constructor that handles both communicating with the update function as well as rendering the contents of the view to HTML.

In the case of shiny_counter, I convert the current state of the model to a string, then I render that count as well as provide two buttons which send defined messages to update the model:

view(model: Model) -> Element(Msg) {
  let count = int.to_string(model)

  html.div(
    [],
    [
      html.p([], [element.text(count)]),
      html.p(
        [],
        [
          html.button([event.on_click(Decrement)], [element.text("-")]),
          html.button([event.on_click(Increment)], [element.text("+")]),
        ],
      ),
    ],
  )
}

From here, I can add styling to make it live up to its shiny_counter name. Here, I’ve added styling to center the text and added the all important ✨ emoji:

fn view(model: Model) -> Element(Msg) {
  let count = int.to_string(model)

  html.div(
    [],
    [
      html.p(
        [attribute.style([#("text-align", "center")])],
        [element.text(count <> " ✨")],
      ),
      html.p(
        [attribute.style([#("text-align", "center")])],
        [
          html.button([event.on_click(Decrement)], [element.text("-")]),
          html.button([event.on_click(Increment)], [element.text("+")]),
        ],
      ),
    ],
  )
}

Start the application

Now that I’ve defined the key parts of a simple Lustre application, I need to combine them into an application. I do that with lustre.simple in this case, since shiny_counter doesn’t have complex interactive behavior. Once I have the application, I can simply start it:

pub fn main() {
  let app = lustre.simple(init, update, view)
  let assert Ok(dispatch) = lustre.start(app, "[gleam_example]", Nil)

  dispatch
}

lustre.start starts an application anchored on an HTML element specified by a CSS selector. In this case [gleam_example] finds an element that has the gleam_example attribute:

<div gleam_example>
</div>

Lustre will replace the selected element entirely with the running application after it loads.When I initially tried Lustre, the example I borrowed from used "body" as the CSS selector, and completely wiped out the template of my blog, making it awfully hard to write this article.I could tell that I misconfigured Lustre, because my blog was briefly visible until the Javascript loaded. This blog post is about embedding Gleam in my blog, not replacing my blog with Gleam. In this case, I chose an attribute since it’s unlikely that I’ll have anything else on the page with that custom attribute.

Now that I have a Lustre application that does what I want, I need to embed it in my blog.

While Gleam does transpile to Javascript it generates a number of different modules that are tedious to use in my static site generator. Enter Vite as a way to bundle the Javascript into a single file:

Step 2: Install and configure Vite

Vite is a Javascript bundling tool, it’s explicitly designed to transform Node libraries or other Javascript modules and make them easy to use in a browser.Here’s a link to the shell.nix that I used for this project.

I can install Vite with npm:

npm install vite

Vite is designed for Javascript and not for Gleam, and so I need to rely on a plugin to show Vite where Gleam’s transpiled Javascript is. I can add that plugin with npm:Thanks to a member of the Gleam community for creating the vite-gleam plugin.

npm add vite-gleam

Now that I’ve added vite-gleam I can create a config file to use it:The rollupOptions.input setting allows me to change the entry file for Vite. By default this is index.html, but since I’m going to be using the bundled Javascript in this blog post, I don’t need a generated index.html file. This setting is described in the Backend Integration instructions for Vite.The output.entryFileNames setting allows me to precisely specify the name of the generated Javascript file. By default, the generated Javascript has a randomly generated name suitable for cache busting. Since I’m copying the generated Javascript file from my Gleam project into my static site generator, I wanted a specific filename to reference in the embedded <script> tag.

import gleam from "vite-gleam";

export default {
  plugins: [gleam()],
  build: {
    rollupOptions: {
      input: 'main.js',
      output: {
        dir: './dist',
        entryFileNames: 'assets/gleam_vite_example.js',
      }
    }
  }
}

Then I create a file called main.js to act as the entry point for Vite:

import { main } from './src/shiny_counter.gleam'

document.addEventListener("DOMContentLoaded", () => {
  const dispatch = main({});
});

Now I can finally call vite buildBy default, Gleam transpiles to Erlang, but vite-gleam will use --target javascript to generate the Javascript code before bundling.If you would like to transpile to Javascript without Vite, then set target = "javascript" in the project’s Gleam.toml., this will use the vite-gleam plugin to first transpile the Gleam code to Javascript, then it will use the main.js entry point to bundle all of the modules into a single file named gleam_vite_example.js.

Step 3: Use the bundled Javascript in my Static Site

Now I can copy the Javascript from my Gleam project into my static site generator. Once there, I can add markup to my site to use it.

Djot, the markup language that I use for my blog allows me to do an HTML passthrough block:

```=html
<div style="border: 1px solid">
  <div gleam_example>
    <script type="module" src="/assets/gleam_vite_example.js"></script>
  </div>
</div>
```

Here, I’ve specified a <div> tag with the gleam_example attribute, added a script tag to use the bundled Gleam Javascript, and then wrapped both in a stylized div.

As mentioned in Step 1, Lustre will replace the attributed div with the running Lustre application after it starts up, leaving me with an embedded interactive component.

Step 4: Publish

It works!You can see the source code for shiny_counter here.

It’s really neat that I can take Gleam and embed it directly into my blog posts. Thank you to everyone in the Gleam discord for helping me figure all of this out.


Constellation Webring