Quickstart#
Lustre is a frontend web framework for Gleam. It is primarily focused on helping you build robust single-page applications (SPAs), but it can also be used on the server to render static HTML. To get an idea of what it's all about, here's a quick overview of Lustre's key features:
Elm-inspired runtime with state management and controlled side effects out of the box.
A simple, declarative API for building type-safe user interfaces.
Stateful components built as custom elements and useable just like any other HTML element.
Static HTML rendering anywhere Gleam can run: the BEAM, Node.js, Deno, or the browser.
In this quickstart guide we'll take a look at how to get up and running with Lustre in both the browser and on the server.
In the browser#
javascript
To get started, we'll scaffold a new Gleam project using gleam new
. If you've
found your way to this guide but don't already know what Gleam is you can read
about it over at gleam.run.
$ gleam new lustre_quickstart && cd lustre_quickstart && gleam add lustre
In a real project you probably want to use a build tool like vite along with the vite-gleam plugin, but to keep this guide simple we'll just show you what code you need to write and leave the details on serving the app up to you. MDN have a handy guide covering some different options to set up a local web server for development if you need some ideas.
Basic HTML setup#
With our Gleam project scaffolded, go ahead and create an index.html
in the root
of the project. This is the minimal code you'll typically want to get started:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lustre Quickstart</title>
<script type="module">
import { main } from "./build/dev/javascript/lustre_quickstart/app.mjs";
document.addEventListener("DOMContentLoaded", () => {
main();
});
</script>
</head>
<body>
<div data-lustre-app></div>
</body>
</html>
We wait until the DOM has loaded before calling the our app's main
function.
This will mount the Lustre app and start rendering. We also add the data-lustre-app
attribute to the element we want to mount the app to. You could use a class or an
id instead, or none of that: lustre.start
takes a CSS
selector so go wild!
Hello, world!#
Go ahead and rename the generated lustre_quickstart.gleam
file to app.gleam
and replace the contents with the following:
import lustre
import lustre/element.{text}
pub fn main() {
let app = lustre.element(text("Hello, world!"))
let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil)
Nil
}
This will create a static Lustre app and mount it onto the element that matches
the CSS selector. While we're asserting everything is OK here, it is possible
for lustre.start
to fail in a couple of ways. Check
out the docs for the lustre.Error
type if you
want to know more.
Run gleam build
and serve the HTML with your preferred static file server (this
step is necessary: JavaScript modules can't be imported when just opening a HTML
file) and admire your handiwork.
Adding interactivity#
Now that we know how to get things up and running, let's try something a little
more exciting and add some interactivity. Replace the contents of your app.gleam
file with the code below and rebuild the project.
import gleam/int
import lustre
import lustre/element.{text}
import lustre/element/html.{div, button, p}
import lustre/event.{on_click}
pub fn main() {
let app = lustre.simple(init, update, view)
let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil)
Nil
}
fn init(_) {
0
}
type Msg {
Incr
Decr
}
fn update(model, msg) {
case msg {
Incr -> model + 1
Decr -> model - 1
}
}
fn view(model) {
let count = int.to_string(model)
div([], [
button([on_click(Decr)], [text(" - ")]),
p([], [text(count)]),
button([on_click(Incr)], [text(" + ")])
])
}
You should now have a very exciting counter app! Almost every Lustre app will boil down to the same three parts:
A
Model
type that represents your application's state and a function toinit
it.A
Msg
type and anupdate
function to update that state based on incoming messages.A
view
function that takes the current state and renders some HTML.
This architecture is not unique to Lustre. It was introduced by the Elm community and known as the Elm Architecture before making its way to React as Redux and beyond, known more generally as the Model-View-Update architecture. If you work through the rest of our guides you'll see how this architecture helps keep side effects out of our view code and how to create components that can encapsulate their own state and update logic.
For now though, we'll leave things here. If you're interested in seeing how Lustre can be used to render static HTML on the server, read on! Otherwise, you can take this counter application as a base and start building something of your own.
On the server#
erlangjavascript
As we've seen, Lustre is primarily meant to be used in the browser to build interactive SPAs. It is possible to render Lustre elements to static HTML and simply use Lustre as a templating DSL. As before, we'll start by scaffolding a new Gleam project and adding Lustre as a dependency:
$ gleam new lustre_quickstart && cd lustre_quickstart && gleam add lustre
The lustre/element
module contains functions to
render an element as either a String
or StringBuilder
. Copy the following code
into lustre_quickstart.gleam
:
import gleam/io
import lustre/attribute.{attribute}
import lustre/element.{text}
import lustre/element/html.{html, head, title, body, div, h1}
pub fn main() {
html([attribute("lang", "en")], [
head([], [
title([], [text("Lustre Quickstart")])
]),
body([], [
h1([], [text("Hello, world!")])
])
])
|> element.to_string
|> io.println
}
We can test this out by running gleam run
and seeing the HTML printed to the
console. From here we could set up a web server using Mist
or Wisp to serve the HTML to the browser or write it to a file
using simplifile. Because the API is the same
for both client and server rendering, it is easy to create reusable components
that can be rendered anywhere Gleam can run!
An example with Wisp#
Before we go, let's just take a quick look at what it would look like to use Lustre in a Wisp application. We won't scaffold out a real app in this example, but we'll adapt one of the examples from Wisp's own documentation.
Specifically, we'll take a look at the show_form
function from the
"working with form data"
example:
...
pub fn show_form() {
// In a larger application a template library or HTML form library might
// be used here instead of a string literal.
let html =
string_builder.from_string(
"<form method='post'>
<label>Title:
<input type='text' name='title'>
</label>
<label>Name:
<input type='text' name='name'>
</label>
<input type='submit' value='Submit'>
</form>",
)
wisp.ok()
|> wisp.html_body(html)
}
They've helpfully left a comment telling us that in a larger application we might want to use a template library, and Lustre is up to the task! Let's refactor this using Lustre:
import gleam/string
import lustre/attribute.{attribute}
import lustre/element
import lustre/element/html
...
pub fn show_form() {
html.form([attribute("method", "post")], [
labelled_input("Title"),
labelled_input("Name"),
html.input([attribute("type", "submit"), attribute("value", "Submit")])
])
|> element.to_string_builder
|> wisp.html_body
|> wisp.ok
}
fn labelled_input(name) {
html.label([], [
element.text(name <> ": "),
html.input([
attribute("type", "text"),
attribute("name", string.lowercase(name))
])
])
}