lustre#
Applications#
On the client, Lustre applications are built on the Model-View-Update architecture. This pattern was popularised by the Elm programming language before being adopted by other state mangement libraries like Redux and Vuex.
Your applications will be made up of three fundamental parts:
A
Model
that represents the entire state of your application and aninit
function to create it.A
Msg
type that represents all the ways the outside world can communicate with your application and anupdate
function that that modifies the model in response to these messages.A
view
function that renders the current state of your application to the DOM.
┌--------+
| |
| update |
| |
+--------+
^ |
| |
Msg | | #(Model, Effect(Msg))
| |
| v
┌------+ ┌------------------------+
| | #(Model, Effect(Msg)) | |
| init |------------------------>| Lustre Runtime |
| | | |
+------+ +------------------------+
^ |
| |
Msg | | Model
| |
| v
┌--------+
| |
| view |
| |
+--------+
App#
erlangjavascript
pub type App(flags, model, msg)
The App
type represents all the parts that make up a Lustre program in the
Model-View-Update architecture along with the runtime necessary to run it.
Although the type itself is exposed to both the Erlang and JavaScript targets,
the functions in this module to construct an App
are only available in the
JavaScript target, and start
will only succeed when ran in the browser.
In the future we may have a way to run Lustre applications on the backend, if you have any ideas on how to achieve this I'd love to hear about them!
Error#
erlangjavascript
pub type Error {
AppAlreadyStarted
AppNotYetStarted
BadComponentName
ComponentAlreadyRegistered
ElementNotFound
NotABrowser
}
The Error
type represents all the ways that a Lustre program can fail. These
include things like trying to start an application that has already been started,
registering a component with a name that is not valid, or trying to start an
application in a context that is not a browser.
Often you will want to perform a couple of these actions together, and unifying
the error type makes this easy. In many of the examples we let assert
that the
result is Ok
but if you wanted to be a bit more dilligent you might use
result.try
instead:
import gleam/result
import lustre
pub fn main () {
use _ <- result.try(lustre.component("my-component", ...))
let app = lustre.application(...)
use dispatch <- result.try(lustre.start(app, "[data-lustre-app]", Nil))
...
}
element#
javascript
pub fn element(el: Element(msg)) -> App(Nil, Nil, msg)
An element
application is the simplest kind of Lustre program. It takes an
Element
to render and renders it to the DOM. These applications hold no state
and do not respond to messages, but that doesn't mean they are not interactive!
It is possible for components
to be rendered inside an
element
application, and these components can be interactive with their own
contained state and update loops.
simple#
javascript
pub fn simple(
init: fn(flags) -> model,
update: fn(model, msg) -> model,
view: fn(model) -> Element(msg)
) -> App(flags, model, msg)
A simple
program introduces the Model-View-Update architecture but leaves out
the ability to dispatch side effects. This means your programs are interactive
but cannot talk to the outside world.
application#
javascript
pub fn application(
init: fn(flags) -> #(model, Effect(msg)),
update: fn(model, msg) -> #(model, Effect(msg)),
view: fn(model) -> Element(msg)
) -> App(flags, model, msg)
The application
constructor is the most complete way to build a Lustre app. As
with simple
it uses the Model-View-Update architecture, but now your
init and update functions can return side effects to be performed by the runtime
in the form of an Effect
.
start#
javascript
pub fn start(
app: App(flags, model, msg),
selector: String,
flags: flags,
) -> Result(fn(msg) -> Nil, Error)
Start an application by providing a CSS selector to find the element to mount the
application onto and any flags to pass to the application on first init. This
function returns a Result
and may fail for a number of reasons. Check out the
Error
type for more information.
destroy#
javascript
pub fn destroy(app: App(flags, model, msg)) -> Result(Nil, Error)
Tear down a running application and remove it from the DOM. This can fail if the application has not yet been started.
Components#
Components take the same Model-View-Update building blocks used to create Lustre applications and allow them to be used as reusable stateful components. This is slightly different to how components are used in other frameworks like React where "component" refers more generally to any reusable piece of UI.
In Lustre, functions that return an Element
are known as "view functions" and
components are more specific abstractions that encapsulate state and behaviour
you might not want to deal with in your top-level application.
Resist the urge to reach for components too early. The Elm community has managed to make do without components at all: you can get surprisingly far storing state in your top level application and passing it down to different view functions. This comes with the added benefit of it being much easier to reason about your UI as a whole.
component#
javascript
pub fn component(
name: String,
init: fn() -> #(model, Effect(msg)),
update: fn(model, msg) -> #(model, Effect(msg)),
view: fn(model) -> Element(msg),
on_attribute_change: Map(String, Decoder(msg)),
) -> Result(Nil, Error)
Register a component with the runtime from the familiar Model-View-Update building blocks. Compared to an application, we have two additional arguments:
A name for the component. This name must follow the same rules laid out in the custom element spec and should contain a hyphen (
-
) to avoid clashes with built-in HTML elements.A map of attribute names to listen for changes to and a decoder for each to decode those attributes into messages to send to your component's
update
function.
If it feels like the API for registering components is a little more verbose than you're used to, that's because it is! You can get surprisingly far storing state in your top level application and passing it down to different view functions without needing to use components at all. In fact, for communities like Elm this is the only way to do things.
Utilities#
is_browser#
erlangjavascript
pub fn is_browser() -> Bool
Gleam has conditional compilation depending on whether you are targetting Erlang or JavaScript, but sometimes you want to be a bit more specific than that and check if you're running in the browser.
This is a runtime check that will tell you just that. You could use this to create a view function that renders something simple on the backend but more complex or interactive on the frontend.
is_registered#
erlangjavascript
pub fn is_registered(name: String) -> Bool
Lustre's components are built directly on the custom element spec which means they share the same global registery as other custom elements. This function can tell you if the name you want to use is already registered, by another Lustre component or otherwise.