Current section

Files

Jump to
lustre src lustre.gleam
Raw

src/lustre.gleam

//// 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](https://github.com/lustre-labs/lustre/tree/main/examples)
//// or the [quickstart guide]() to get up to speed!
////
//// Lustre currently has three kinds of application:
////
//// 1. 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.
////
//// 2. 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.
////
//// 3. 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](./lustre/server_component.html#script) 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 an `init` function
//// to create it.
////
//// - A `Msg` type that represents all the different ways the outside world can
//// communicate with your application and an `update` function that modifies
//// your model in response to those messages.
////
//// - A `view` function that renders your model to HTML, represented as an
//// `Element`.
////
//// To see how those pieces fit together, here's a little diagram:
////
//// ```text
//// +--------+
//// | |
//// | 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`](./effect.html) 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:
////
//// - [`01-quickstart`](/guide/01-quickstart.html)
////
//// 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](https://github.com/lustre-labs/lustre)
//// that each demonstrate a different concept or idea:
////
//// - [`01-hello-world`](https://github.com/lustre-labs/lustre/tree/main/examples/01-hello-world)
//// - [`02-interactivity`](https://github.com/lustre-labs/lustre/tree/main/examples/02-interactivity)
//// - [`03-controlled-inputs`](https://github.com/lustre-labs/lustre/tree/main/examples/03-controlled-inputs)
//// - [`04-custom-event-handlers`](https://github.com/lustre-labs/lustre/tree/main/examples/04-custom-event-handlers)
//// - [`05-http-requests`](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests)
//// - [`06-custom-effects`](https://github.com/lustre-labs/lustre/tree/main/examples/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](https://github.com/lustre-labs/ui) is a collection of pre-designed
//// elements and design tokens for building user interfaces with Lustre.
////
//// - [lustre/ssg](https://github.com/lustre-labs/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](https://discord.gg/Fm8Pwmy).
//// You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
////
//// While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org)
//// 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](https://discord.gg/Fm8Pwmy).
//// You can also tag Hayleigh on Twitter [@hayleigh-dot-dev](https://twitter.com/hayleighdotdev)
//// or on BlueSky [@hayleigh.dev](https://bsky.app/profile/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](https://github.com/lustre-labs/lustre/issues).
//// Fixes and improvements to the documentation are also very welcome!
////
//// Finally, if you'd like, you can support the project through
//// [GitHub Sponsors](https://github.com/sponsors/hayleigh-dot-dev). Sponsorship
//// helps fund the copious amounts of coffee that goes into building and maintaining
//// Lustre, and is very much appreciated!
////
// IMPORTS ---------------------------------------------------------------------
import gleam/bool
import gleam/dict.{type Dict}
import gleam/dynamic.{type Decoder}
import gleam/erlang/process.{type Subject}
import gleam/option.{type Option, None, Some}
import gleam/otp/actor.{type StartError}
import gleam/result
import lustre/effect.{type Effect}
import lustre/element.{type Element}
import lustre/internals/patch
import lustre/internals/runtime
// TYPES -----------------------------------------------------------------------
/// 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`](#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](https://github.com/lustre-labs/lustre/tree/main/examples) or the
/// [quickstart guide]()
///
/// - Use [`start_server_component`](#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`](#start_actor) to start a Lustre Server Component only
/// for the Erlang target. BEAM users should always prefer this over
/// `start_server_component` so they can take advantage of OTP features.
///
/// - Use [`register`](#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`](./lustre/element.html#to_string) function.
///
pub opaque type App(flags, model, msg) {
App(
init: fn(flags) -> #(model, Effect(msg)),
update: fn(model, msg) -> #(model, Effect(msg)),
view: fn(model) -> Element(msg),
// The `dict.mjs` module in the standard library is huge (20+kb!). For folks
// that don't ever build components and don't use a dictionary in any of their
// code we'd rather not thrust that increase in bundle size on them just to
// call `dict.new()`.
//
// Using `Option` here at least lets us say `None` for the empty case in the
// `application` constructor.
//
on_attribute_change: Option(Dict(String, Decoder(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`](#Action) type to stop you accidentally
/// sending actions to the wrong kind of runtime.
///
pub type ClientSpa
/// 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`](#Action) type to stop you accidentally
/// sending actions to the wrong kind of runtime.
///
pub type ServerComponent
/// 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`](#dispatch) action.
///
/// The `runtime` type parameter represents the type of Lustre application that
/// can receive this action. If we [`start`](#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`](#ClientSpa)
/// runtime, and trying to send actions like [`add_renderer`](#add_renderer) would
/// result in a type error.
///
pub type Action(msg, runtime) =
runtime.Action(msg, runtime)
/// 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)
/// 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 ----------------------------------------------------------------
/// 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`](#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 element(html: Element(msg)) -> App(Nil, Nil, msg) {
let init = fn(_) { #(Nil, effect.none()) }
let update = fn(_, _) { #(Nil, effect.none()) }
let view = fn(_) { html }
application(init, update, view)
}
/// 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`](#application) constructor instead.
///
pub fn simple(
init: fn(flags) -> model,
update: fn(model, msg) -> model,
view: fn(model) -> Element(msg),
) -> App(flags, model, msg) {
let init = fn(flags) { #(init(flags), effect.none()) }
let update = fn(model, msg) { #(update(model, msg), effect.none()) }
application(init, update, view)
}
/// 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`](./lustre/effect.html) module or the
/// [HTTP requests example](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests).
///
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) {
App(init, update, view, None)
}
/// 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`](#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](./lustre/element.html#Element))
/// and only reach for components when you really need to encapsulate that update
/// loop.
///
pub fn component(
init: fn(flags) -> #(model, Effect(msg)),
update: fn(model, msg) -> #(model, Effect(msg)),
view: fn(model) -> Element(msg),
on_attribute_change: Dict(String, Decoder(msg)),
) -> App(flags, model, msg) {
App(init, update, view, Some(on_attribute_change))
}
// EFFECTS ---------------------------------------------------------------------
/// 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](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
/// 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(
app: App(flags, model, msg),
onto selector: String,
with flags: flags,
) -> Result(fn(Action(msg, ClientSpa)) -> Nil, Error) {
use <- bool.guard(!is_browser(), Error(NotABrowser))
do_start(app, selector, flags)
}
@external(javascript, "./lustre.ffi.mjs", "start")
fn do_start(
_app: App(flags, model, msg),
_selector: String,
_flags: flags,
) -> Result(fn(Action(msg, ClientSpa)) -> Nil, Error) {
// It should never be possible for the body of this function to execute on the
// Erlang target because the `is_browser` guard will prevent it. Instead of
// a panic, we still return a well-typed `Error` here in the case where someone
// mistakenly uses this function internally.
Error(NotABrowser)
}
/// 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`](#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`](#shutdown) action is sent to it.
///
/// **Note**: Users running their application on the BEAM should use [`start_actor`](#start_actor)
/// instead to make use of Gleam's OTP abstractions.
///
@external(javascript, "./lustre.ffi.mjs", "start_server_application")
pub fn start_server_component(
app: App(flags, model, msg),
with flags: flags,
) -> Result(fn(Action(msg, ServerComponent)) -> Nil, Error) {
use runtime <- result.map(start_actor(app, flags))
actor.send(runtime, _)
}
/// 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`](https://hexdocs.pm/gleam_erlang/gleam/erlang/process.html#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`](#start_server_component)
/// instead.
///
pub fn start_actor(
app: App(flags, model, msg),
with flags: flags,
) -> Result(Subject(Action(msg, ServerComponent)), Error) {
do_start_actor(app, flags)
}
@target(javascript)
fn do_start_actor(_, _) {
Error(NotErlang)
}
@target(erlang)
fn do_start_actor(
app: App(flags, model, msg),
flags: flags,
) -> Result(Subject(Action(msg, ServerComponent)), Error) {
let on_attribute_change = option.unwrap(app.on_attribute_change, dict.new())
app.init(flags)
|> runtime.start(app.update, app.view, on_attribute_change)
|> result.map_error(ActorError)
}
/// 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](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names)
/// 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`](#start_server_component)
/// or [`start_actor`](#start_actor) instead.
///
@external(javascript, "./lustre.ffi.mjs", "make_lustre_client_component")
pub fn register(_app: App(Nil, model, msg), _name: String) -> Result(Nil, Error) {
Error(NotABrowser)
}
// ACTIONS ---------------------------------------------------------------------
/// 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 dispatch(msg: msg) -> Action(msg, runtime) {
runtime.Dispatch(msg)
}
/// 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 shutdown() -> Action(msg, runtime) {
runtime.Shutdown
}
// UTILS -----------------------------------------------------------------------
/// 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!
///
@external(javascript, "./lustre.ffi.mjs", "is_browser")
pub fn is_browser() -> Bool {
False
}
/// 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.
///
@external(javascript, "./lustre.ffi.mjs", "is_registered")
pub fn is_registered(_name: String) -> Bool {
False
}