Current section
Files
Jump to
Current section
Files
src/agnostic.gleam
//// Lustre is a library 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](./guide/01-quickstart.html) 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 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](./agnostic/server_component.html#script) or serve
//// the pre-bundled scripts found in 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 `Message` 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 |
//// | |
//// +--------+
//// ^ |
//// | |
//// Message | | #(Model, Effect(Message))
//// | |
//// | v
//// +------+ +------------------------+
//// | | #(Model, Effect(Message)) | |
//// | init |-------------------------->| Lustre Runtime |
//// | | | |
//// +------+ +------------------------+
//// ^ |
//// | |
//// message | | 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)
//// - [`02-state-management`](./guide/02-state-management.html)
//// - [`03-side-effects`](./guide/03-side-effects.html)
//// - [`04-spa-deployments`](./guide/04-spa-deployments.html)
//// - [`05-server-side-rendering`](./guide/05-server-side-rendering.html)
//// - [`06-full-stack-applications`](./guide/06-full-stack-applications.html)
//// - [`07-full-stack-deployments`](./guide/07-full-stack-deployments.html)
//// - [`08-components`](./guide/08-components.html)
//// - [`09-server-components`](./guide/09-server-components.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. While we can't list them
//// all here, some of the more important ones are:
////
//// - [`Controlled inputs`](https://github.com/lustre-labs/lustre/tree/main/examples/02-inputs/01-controlled-inputs)
//// - [`Handling forms`](https://github.com/lustre-labs/lustre/tree/main/examples/02-inputs/04-forms)
//// - [`Making HTTP requests`](https://github.com/lustre-labs/lustre/tree/main/examples/03-effects/01-http-requests)
//// - [`Routing`](https://github.com/lustre-labs/lustre/tree/main/examples/04-applications/01-routing)
//// - [`Creating components`](https://github.com/lustre-labs/lustre/tree/main/examples/05-components/01-basic-setup)
//// - [`Creating server components`](https://github.com/lustre-labs/lustre/tree/main/examples/06-server-components/01-basic-setup)
////
//// ## 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).
////
//// ## 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 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 agnostic/component.{type Option}
import agnostic/effect.{type Effect}
import agnostic/element.{type Element}
import agnostic/platform.{type Platform}
import agnostic/platform/dom
import agnostic/runtime/app.{App}
import agnostic/runtime/headless
import gleam/erlang/process.{type Name, type Subject}
import gleam/option
import gleam/otp/actor
import gleam/otp/factory_supervisor.{type Builder}
import gleam/otp/supervision.{type ChildSpecification}
// 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
/// or a server component anywhere Gleam runs. Pass the appropriate
/// [`Platform`](./agnostic/platform.html#Platform) to control where the app runs.
///
/// 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 [`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
/// [`dom.to_string`](./agnostic/platform/dom.html#to_string) function.
///
pub type App(arguments, model, message) =
app.App(arguments, model, message)
/// 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(reason: actor.StartError)
BadComponentName(name: String)
ComponentAlreadyRegistered(name: String)
ElementNotFound(selector: String)
NotABrowser
NotMountable
}
/// Once you start a Lustre application, you get back a `Runtime` you can later
/// use to send messages to your application using the [`dispatch`](#dispatch)
/// function.
///
pub type Runtime(message)
/// From outside your Lustre applications, it is possible to communicate with the
/// runtime by sending more than just messages to your app's `update` function.
/// Communication to the runtime itself is mediated by the `RuntimeMessage` type
/// and can be constructed in a few different ways:
///
/// - [`dispatch`](#dispatch) lets you send a message to your app's `update`
/// function the same way effects do.
///
/// - [`shutdown`](#shutdown) instructs a running application to stop and clean
/// up. For JavaScript applications, this is often imperfect and may leave an
/// empty shell app running. For Erlang server components, this will stop the
/// process.
///
/// - When running a server component, you can decode messages from the client
/// runtime using [`runtime_message_decoder`](./agnostic/server_component.html#runtime_message_decoder)
/// and [`send`](#send) them manually.
///
pub type RuntimeMessage(message) =
headless.Message(message)
// CONSTRUCTORS ----------------------------------------------------------------
/// The simplest type of Lustre application. The `element` application is
/// primarily used for demonstration purposes. It renders a static Lustre `Element`
/// on the page and does not have any state or update logic.
///
pub fn element(view: Element(message)) -> App(arguments, Nil, message) {
application(
init: fn(_) { #(Nil, effect.none()) },
update: fn(_, _) { #(Nil, effect.none()) },
view: fn(_) { 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 init: fn(arguments) -> model,
update update: fn(model, message) -> model,
view view: fn(model) -> Element(message),
) -> App(arguments, model, message) {
let init = fn(arguments) { #(init(arguments), effect.none()) }
let update = fn(model, message) { #(update(model, message), 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`](./agnostic/effect.html) module or the
/// [HTTP requests example](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests).
///
pub fn application(
init init: fn(arguments) -> #(model, Effect(message)),
update update: fn(model, message) -> #(model, Effect(message)),
view view: fn(model) -> Element(message),
) -> App(arguments, model, message) {
App(name: option.None, init:, update:, view:, config: app.default_config)
}
/// 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 take a bit more set up than components in JavaScript
/// > 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](./agnostic/element.html#Element))
/// > and only reach for components when you really need to encapsulate that update
/// > loop.
///
pub fn component(
init init: fn(arguments) -> #(model, Effect(message)),
update update: fn(model, message) -> #(model, Effect(message)),
view view: fn(model) -> Element(message),
options options: List(Option(message)),
) -> App(arguments, model, message) {
App(name: option.None, init:, update:, view:, config: app.configure(options))
}
/// Assign a [`Name`](https://hexdocs.pm/gleam_erlang/gleam/erlang/process.html#Name)
/// to a Lustre application. This is useful for [_supervised_](#supervised) server
/// components as it allows other processes to find and communicate with the
/// runtime even if it is restarted.
///
/// > **Note**: names must **never** be created dynamically as too many names
/// > will exhaust the atom table and cause the VM to crash. Names should be
/// > created at the start of your program and passed down where needed.
///
/// > **Note**: a named application should **never** be used to create a
/// > [factory supervisor](#factory) as only one process can be registered under
/// > a given name.
///
pub fn named(
app: App(arguments, model, message),
name: Name(RuntimeMessage(message)),
) -> App(arguments, model, message) {
App(..app, name: option.Some(name))
}
// RUNTIME ---------------------------------------------------------------------
/// Start a constructed application. The platform determines where and how the
/// application runs:
///
/// - Use [`platform.dom`](./agnostic/platform.html#dom) to start a client-side
/// single-page application (SPA) in the browser.
///
/// - Use [`platform.headless`](./agnostic/platform.html#headless) to start a
/// server component that sends patches to connected clients.
///
/// The `arguments` argument is the starting data for the application, passed
/// to the application's `init` function.
///
pub fn start(
app: App(arguments, model, message),
on platform: Platform(node, target, value, event, message, raw),
with arguments: arguments,
) -> Result(Runtime(message), Error) {
case platform.is_headless(platform) {
True -> do_start_headless(app, arguments)
False -> {
case platform.mount(platform) {
Ok(#(root, initial_vdom)) ->
Ok(do_start_rendered(root, initial_vdom, app, platform, arguments))
Error(platform.NotABrowser) -> Error(NotABrowser)
Error(platform.ElementNotFound(sel)) -> Error(ElementNotFound(sel))
Error(platform.NotMountable) -> Error(NotMountable)
}
}
}
}
@external(javascript, "./agnostic/runtime/platform.ffi.mjs", "start")
fn do_start_rendered(
_root: node,
_initial_vdom: element.Element(message),
_app: App(arguments, model, message),
_platform: Platform(node, target, value, event, message, raw),
_start_args: arguments,
) -> Runtime(message) {
panic as "Rendered runtime not yet implemented for Erlang"
}
@external(javascript, "./agnostic/runtime/headless.ffi.mjs", "start")
fn do_start_headless(
app: App(arguments, model, message),
arguments: arguments,
) -> Result(Runtime(message), Error) {
let result =
headless.start(
app.name,
app.init,
app.update,
app.view,
app.configure_server_component(app.config),
arguments,
)
case result {
Ok(actor.Started(data: subject, ..)) -> Ok(hide_subject(subject))
Error(error) -> Error(ActorError(error))
}
}
/// Create a server component child specification suitable for supervision in a
/// [static supervisor](https://hexdocs.pm/gleam_otp/gleam/otp/static_supervisor.html).
/// This is the preferred way of starting Lustre server components on the Erlang
/// target.
///
pub fn supervised(
app: App(arguments, model, message),
arguments: arguments,
) -> ChildSpecification(Subject(RuntimeMessage(message))) {
use <- supervision.worker
headless.start(
app.name,
app.init,
app.update,
app.view,
app.configure_server_component(app.config),
arguments,
)
}
/// Create a [factory supervisor](https://hexdocs.pm/gleam_otp/gleam/otp/factory_supervisor.html)
/// capable of starting many instances of a Lustre server component dynamically.
/// Along with [`supervised`](#supervised), this is one of the ways to ensure
/// proper supervision and fault-tolerance for Lustre server components on the
/// Erlang target.
///
pub fn factory(
app: App(arguments, model, message),
) -> Builder(arguments, Subject(RuntimeMessage(message))) {
use arguments <- factory_supervisor.worker_child
headless.start(
app.name,
app.init,
app.update,
app.view,
app.configure_server_component(app.config),
arguments,
)
}
/// 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` arguments
/// because there is no way to provide an initial value for arguments when using a
/// Custom Element!
///
/// The name 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", [], [])`.
///
/// Each component instance automatically gets its own DOM platform constructed
/// from its shadow root, so no platform argument is needed.
///
/// > **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 start a server component using [`start`](#start) with
/// > [`platform.headless`](./agnostic/platform.html#headless) instead.
///
pub fn register(
app: App(Nil, model, message),
named name: String,
) -> Result(Nil, Error) {
do_register(app, dom.platform_strict, name)
}
@external(javascript, "./agnostic/runtime/web_component.ffi.mjs", "make_component")
fn do_register(
_app: App(Nil, model, message),
_make_platform: fn(dom.DomNode) ->
Platform(
dom.DomNode,
dom.DomNode,
dom.DomNode,
dom.DomEvent,
message,
dom.DomNode,
),
_name: String,
) -> Result(Nil, Error) {
Error(NotABrowser)
}
// MESSAGES --------------------------------------------------------------------
/// Send a message to a running application's runtime directly. This function is
/// primarily used for sending decoded client messages to a server component's
/// runtime.
///
@external(erlang, "gleam@erlang@process", "send")
@external(javascript, "./agnostic/runtime/platform/base.ffi.mjs", "send")
pub fn send(
to runtime: Runtime(message),
message message: RuntimeMessage(message),
) -> Nil
/// Build a message for a running application's `update` function.
///
/// This message can be delivered to the runtime using [`send`](#send), allowing
/// communication with a Lustre app without having to use an effect.
///
pub fn dispatch(message: message) -> RuntimeMessage(message) {
headless.EffectDispatchedMessage(message)
}
/// 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() -> RuntimeMessage(message) {
headless.SystemRequestedShutdown
}
// UTILS -----------------------------------------------------------------------
/// Check if the application is running in the browser.
///
@external(javascript, "./agnostic/runtime/platform/base.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, "./agnostic/runtime/platform/base.ffi.mjs", "is_registered")
pub fn is_registered(_name: String) -> Bool {
False
}
@external(erlang, "gleam@function", "identity")
@external(javascript, "../gleam_stdlib/gleam/function.mjs", "identity")
fn hide_subject(subject: Subject(RuntimeMessage(message))) -> Runtime(message)