lustre
Lustre is a framework for rendering Web applications and components using Gleam. This module contains the core API for constructing and communicating with Lustre applications. If you’re new to Lustre or frontend development in general, make sure you check out the examples or the quickstart guide to get up to speed!
Lustre currently has three kinds of application:
-
A client-side single-page application: think Elm or React or Vue. These are applications that run in the client’s browser and are responsible for rendering the entire page.
-
A client-side component: an encapsulated Lustre application that can be rendered inside another Lustre application as a Web Component. Communication happens via attributes and event listeners, like any other encapsulated HTML element.
-
A server component. These are applications that run anywhere Gleam runs and communicate with any number of connected clients by sending them patches to apply to their DOM.
There are two pieces to a server component: the main server component runtime that contains your application logic, and a client-side runtime that listens for patches over a WebSocket and applies them to the DOM.
The server component runtime can run anywhere Gleam does, but the client-side runtime must be run in a browser. To use it either render the provided script element or use the script files from Lustre’s
priv/
directory directly.
No matter where a Lustre application runs, it will always follow the same Model-View-Update architecture. Popularised by Elm (where it is known as The Elm Architecture), this pattern has since made its way into many other languages and frameworks and has proven to be a robust and reliable way to build complex user interfaces.
There are three main building blocks to the Model-View-Update architecture:
-
A
Model
that represents your application’s state and aninit
function to create it. -
A
Msg
type that represents all the different ways the outside world can communicate with your application and anupdate
function that modifies your model in response to those messages. -
A
view
function that renders your model to HTML, represented as anElement
.
To see how those pieces fit together, here’s a little diagram:
+--------+
| |
| update |
| |
+--------+
^ |
| |
Msg | | #(Model, Effect(Msg))
| |
| v
+------+ +------------------------+
| | #(Model, Effect(Msg)) | |
| init |------------------------>| Lustre Runtime |
| | | |
+------+ +------------------------+
^ |
| |
Msg | | Model
| |
| v
+--------+
| |
| view |
| |
+--------+
The Effect
type here encompasses things like HTTP requests and other kinds
of communication with the “outside world”. You can read more about effects
and their purpose in the effect
module.
For many kinds of apps, you can take these three building blocks and put together a Lustre application capable of running anywhere. Because of that, we like to describe Lustre as a universal framework.
Guides
A number of guides have been written to teach you how to use Lustre to build different kinds of applications. If you’re just getting started with Lustre or frontend development, we recommend reading through them in order:
This list of guides is likely to grow over time, so be sure to check back every now and then to see what’s new!
Examples
If you prefer to learn by seeing and adapting existing code, there are also a number of examples in the Lustre GitHub repository that each demonstrate a different concept or idea:
01-hello-world
02-interactivity
03-controlled-inputs
04-custom-event-handlers
05-http-requests
06-custom-effects
This list of examples is likely to grow over time, so be sure to check back every now and then to see what’s new!
Companion libraries
While this package contains the runtime and API necessary for building and rendering applications, there is also a small collection of companion libraries built to make building Lustre applications easier:
-
lustre/ui is a collection of pre-designed elements and design tokens for building user interfaces with Lustre.
-
lustre/ssg is a simple static site generator that you can use to produce static HTML documents from your Lustre applications.
Both of these packages are heavy works in progress: any feedback or contributions are very welcome!
Getting help
If you’re having trouble with Lustre or not sure what the right way to do something is, the best place to get help is the Gleam Discord server. You could also open an issue on the Lustre GitHub repository.
While our docs are still a work in progress, the official Elm guide is also a great resource for learning about the Model-View-Update architecture and the kinds of patterns that Lustre is built around.
Contributing
The best way to contribute to Lustre is by building things! If you’ve built
something cool with Lustre you want to share then please share it on the
#sharing
channel in the Gleam Discord server.
You can also tag Hayleigh on Twitter @hayleigh-dot-dev
or on BlueSky @hayleigh.dev.
If you run into any issues or have ideas for how to improve Lustre, please open an issue on the Lustre GitHub repository. Fixes and improvements to the documentation are also very welcome!
Finally, if you’d like, you can support the project through GitHub Sponsors. Sponsorship helps fund the copious amounts of coffee that goes into building and maintaining Lustre, and is very much appreciated!
Types
An action represents a message that can be sent to a running Lustre application. Code that is orchestrating an application where Lustre is only one part of the system will likely want to send actions to the Lustre runtime. For most kinds of application, you can usually ignore actions entirely.
The msg
type parameter is the kind of messages you can send to the runtime’s
update
function through the dispatch
action.
The runtime
type parameter represents the type of Lustre application that
can receive this action. If we start
a typical Lustre SPA, we
get back the type Result(fn(Action(msg, ClientSpa)) -> Nil, Error)
. This
means we can only send actions suitable for the ClientSpa
runtime, and trying to send actions like add_renderer
would
result in a type error.
pub type Action(msg, runtime) =
runtime.Action(msg, runtime)
Represents a constructed Lustre application that is ready to be started. Depending on where you want the application to run, you have a few options:
-
Use
start
to start a single-page-application in the browser.This is the most common way to start a Lustre application. If you’re new to Lustre or frontend development in general, make sure you check out the examples or the quickstart guide
-
Use
start_server_component
to start a Lustre Server Component anywhere Gleam will run: Erlang, Node, Deno, or in the browser. If you’re running on the BEAM though, you should… -
Use
start_actor
to start a Lustre Server Component only for the Erlang target. BEAM users should always prefer this overstart_server_component
so they can take advantage of OTP features. -
Use
register
to register a component in the browser to be used as a Custom Element. This is useful even if you’re not using Lustre to build a SPA.
If you’re only interested in using Lustre as a HTML templating engine, you
don’t need an App
at all! You can render an element directly using the
element.to_string
function.
pub opaque type App(flags, model, msg)
The ClientSpa
runtime is the most typical kind of Lustre application: it’s
a single-page application that runs in the browser similar to React or Elm.
This type is used to tag the Action
type to stop you accidentally
sending actions to the wrong kind of runtime.
pub type ClientSpa
Starting a Lustre application might fail for a number of reasons. This error type enumerates all those reasons, even though some of them are only possible on certain targets.
pub type Error {
ActorError(StartError)
BadComponentName(name: String)
ComponentAlreadyRegistered(name: String)
ElementNotFound(selector: String)
NotABrowser
NotErlang
}
Constructors
-
ActorError(StartError)
-
BadComponentName(name: String)
-
ComponentAlreadyRegistered(name: String)
-
ElementNotFound(selector: String)
-
NotABrowser
-
NotErlang
Patches are sent by server components to any connected renderers. Because
server components are not opinionated about your network layer or how your
wider application is organised, it is your responsibility to make sure a Patch
makes its way to the server component client runtime.
pub type Patch(msg) =
patch.Patch(msg)
A ServerComponent
is a type of Lustre application that does not directly
render anything to the DOM. Instead, it can run anywhere Gleam runs and
operates in a “headless” mode where it computes diffs between renders and
sends them to any number of connected listeners.
Lustre Server Components are not tied to any particular transport or network protocol, but they are most commonly used with WebSockets in a fashion similar to Phoenix LiveView.
This type is used to tag the Action
type to stop you accidentally
sending actions to the wrong kind of runtime.
pub type ServerComponent
Functions
pub fn application(
init: fn(a) -> #(b, Effect(c)),
update: fn(b, c) -> #(b, Effect(c)),
view: fn(b) -> Element(c),
) -> App(a, b, c)
A complete Lustre application that follows the Model-View-Update architecture and can handle side effects like HTTP requests or querying the DOM. Most real Lustre applications will use this constructor.
To learn more about effects and their purpose, take a look at the
effect
module or the
HTTP requests example.
pub fn component(
init: fn(a) -> #(b, Effect(c)),
update: fn(b, c) -> #(b, Effect(c)),
view: fn(b) -> Element(c),
on_attribute_change: Dict(
String,
fn(Dynamic) -> Result(c, List(DecodeError)),
),
) -> App(a, b, c)
A component
is a type of Lustre application designed to be embedded within
another application and has its own encapsulated update loop. This constructor
is almost identical to the application
constructor, but it
also allows you to specify a dictionary of attribute names and decoders.
When a component is rendered in a parent application, it can receive data from the parent application through HTML attributes and properties just like any other HTML element. This dictionary of decoders allows you to specify how to decode those attributes into messages your component’s update loop can handle.
Note: Lustre components are conceptually a lot “heavier” than components in frameworks like React. They should be used for more complex UI widgets like a combobox with complex keyboard interactions rather than simple things like buttons or text inputs. Where possible try to think about how to build your UI with simple view functions (functions that return Elements) and only reach for components when you really need to encapsulate that update loop.
pub fn dispatch(msg: a) -> Action(a, b)
Dispatch a message to a running application’s update
function. This can be
used as a way for the outside world to communicate with a Lustre app without
the app needing to initiate things with an effect.
Both client SPAs and server components can have messages sent to them using
the dispatch
action.
pub fn element(html: Element(a)) -> App(Nil, Nil, a)
An element is the simplest type of Lustre application. It renders its contents once and does not handle any messages or effects. Often this type of application is used for folks just getting started with Lustre on the frontend and want a quick way to get something on the screen.
Take a look at the simple
application constructor if you want to
build something interactive.
Note: Just because an element doesn’t have its own update loop, doesn’t mean its content is always static! An element application may render a client or server component that has its own encapsulated update loop!
pub fn is_browser() -> Bool
Gleam’s conditional compilation makes it possible to have different implementations of a function for different targets, but it’s not possible to know what runtime you’re targeting at compile-time.
This is problematic if you’re using server components with a JavaScript backend because you’ll want to know whether you’re currently running on your server or in the browser: this function tells you that!
pub fn is_registered(name: String) -> Bool
Check if the given component name has already been registered as a Custom Element. This is particularly useful in contexts where other web components may have been registered and you must avoid collisions.
pub fn register(
app: App(Nil, a, b),
name: String,
) -> Result(Nil, Error)
Register a Lustre application as a Web Component. This lets you render that
application in another Lustre application’s view or use it as a Custom Element
outside of Lustre entirely.The provided application can only have Nil
flags
because there is no way to provide an initial value for flags when using a
Custom Element!
The second argument is the name of the Custom Element. This is the name you’d
use in HTML to render the component. For example, if you register a component
with the name my-component
, you’d use it in HTML by writing <my-component>
or in Lustre by rendering element("my-component", [], [])
.
Note: There are some rules for what names are valid for a Custom Element. The most important one is that the name must contain a hypen so that it can be distinguished from standard HTML elements.
Note: This function is only meaningful when running in the browser and will
produce a NotABrowser
error if called anywhere else. For server contexts,
you can render a Lustre server component using start_server_component
or start_actor
instead.
pub fn shutdown() -> Action(a, b)
Instruct a running application to shut down. For client SPAs this will stop the runtime and unmount the app from the DOM. For server components, this will stop the runtime and prevent any further patches from being sent to connected clients.
pub fn simple(
init: fn(a) -> b,
update: fn(b, c) -> b,
view: fn(b) -> Element(c),
) -> App(a, b, c)
A simple
application has the basic Model-View-Update building blocks present
in all Lustre applications, but it cannot handle effects. This is a great way
to learn the basics of Lustre and its architecture.
Once you’re comfortable with the Model-View-Update loop and want to start
building more complex applications that can communicate with the outside world,
you’ll want to use the application
constructor instead.
pub fn start(
app: App(a, b, c),
onto selector: String,
with flags: a,
) -> Result(fn(Action(c, ClientSpa)) -> Nil, Error)
Start a constructed application as a client-side single-page application (SPA). This is the most typical way to start a Lustre application and will only work in the browser
The second argument is a CSS selector
used to locate the DOM element where the application will be mounted on to.
The most common selectors are "#app"
to target an element with an id of app
or [data-lustre-app]
to target an element with a data-lustre-app
attribute.
The third argument is the starting data for the application. This is passed
to the application’s init
function.
pub fn start_actor(
app: App(a, b, c),
with flags: a,
) -> Result(Subject(Action(c, ServerComponent)), Error)
Start an application as a server component specifically for the Erlang target.
Instead of receiving a callback on successful start, this function returns
a Subject
Note: This function is only meaningful on the Erlang target. Attempts to
call it on the JavaScript will result in the NotErlang
error. If you’re running
a Lustre server component on Node or Deno, use start_server_component
instead.
pub fn start_server_component(
app: App(a, b, c),
with flags: a,
) -> Result(fn(Action(c, ServerComponent)) -> Nil, Error)
Start an application as a server component. This runs in a headless mode and
doesn’t render anything to the DOM. Instead, multiple clients can be attached
using the add_renderer
action.
If a server component starts successfully, this function will return a callback that can be used to send actions to the component runtime.
A server component will keep running until the program is terminated or the
shutdown
action is sent to it.
Note: Users running their application on the BEAM should use start_actor
instead to make use of Gleam’s OTP abstractions.