diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2024-02-11 16:50:25 +0000 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2024-02-11 16:50:25 +0000 |
commit | c4e9f6a2708940f692066a2a28e6f766c454b80d (patch) | |
tree | 6bb38b90c067a1f372aa9c6d1d626dba2809c339 | |
parent | 978e56bf9b2f0258c341fb7f3a0fcbd2d3a3ac4a (diff) | |
download | lustre-c4e9f6a2708940f692066a2a28e6f766c454b80d.tar.gz lustre-c4e9f6a2708940f692066a2a28e6f766c454b80d.zip |
:memo: Flesh out API docs.
-rw-r--r-- | src/lustre.gleam | 285 |
1 files changed, 230 insertions, 55 deletions
diff --git a/src/lustre.gleam b/src/lustre.gleam index 6c58c85..3151a1a 100644 --- a/src/lustre.gleam +++ b/src/lustre.gleam @@ -1,8 +1,10 @@ //// Lustre is a framework for rendering Web applications and components using //// Gleam. This module contains the core API for constructing and communicating -//// with the different kinds of Lustre application. +//// 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 two kinds of application: +//// 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 @@ -13,13 +15,18 @@ //// happens via attributes and event listeners, like any other encapsulated //// HTML element. //// -//// 3. A Lustre Server Component. These are applications that run anywhere Gleam -//// runs and communicate with any number of connected clients by sending them +//// 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. //// -//// On the server, these applications can be communicated with by sending them -//// messages directly. On the client communication happens the same way as -//// client-side components: through attributes and event listeners. +//// 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#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 @@ -69,21 +76,91 @@ //// +--------+ //// ``` //// -//// ❓ Wondering what that [`Effect`](./effect#effect-type) is all about? Check -//// out the documentation for that over in the [`effect`](./effect) module. +//// 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) module. //// //// For many kinds of app, you can take these three building blocks and put -//// together a Lustre application capable of running *anywhere*. We like to -//// describe Lustre as a **universal framework**. +//// together a Lustre application capable of running *anywhere*. Beacuse 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](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! //// -//// To read the full documentation for this module, please visit -//// [https://lustre.build/api/lustre](https://lustre.build/api/lustre) // IMPORTS --------------------------------------------------------------------- import gleam/bool import gleam/dict.{type Dict} -import gleam/dynamic.{type Decoder, type Dynamic} +import gleam/dynamic.{type Decoder} import gleam/erlang/process.{type Subject} import gleam/otp/actor.{type StartError} import gleam/result @@ -94,14 +171,18 @@ import lustre/internals/runtime // TYPES ----------------------------------------------------------------------- /// Represents a constructed Lustre application that is ready to be started. -/// Depending on the kind of application you've constructed you have a few -/// options: +/// 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. +/// 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 @@ -111,6 +192,10 @@ import lustre/internals/runtime /// 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#to_string) function. +/// pub opaque type App(flags, model, msg) { App( init: fn(flags) -> #(model, Effect(msg)), @@ -120,8 +205,11 @@ pub opaque type App(flags, model, msg) { ) } -/// The `Browser` runtime is the most typical kind of Lustre application: it's -/// a single-page application that runs in the browser similar to React or Vue. +/// 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 @@ -134,17 +222,28 @@ pub type ClientSpa /// 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 (some types of) a running -/// Lustre application. Like the [`App`](#App) type, the `runtime` type parameter -/// can be used to determine what kinds of application a particular action can be -/// sent to. +/// 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 ustre 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(runtime, msg) = - runtime.Action(runtime, msg) +pub type Action(msg, runtime) = + runtime.Action(msg, runtime) /// 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 @@ -169,9 +268,9 @@ pub type Error { /// Take a look at the [`simple`](#simple) application constructor if you want to /// build something interactive. /// -/// 💡 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 component or -/// server component that has its own encapsulated update loop! +/// **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()) } @@ -181,7 +280,13 @@ pub fn element(html: Element(msg)) -> App(Nil, Nil, msg) { 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, @@ -194,7 +299,13 @@ pub fn simple( 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) 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)), @@ -204,7 +315,23 @@ pub fn application( App(init, update, view, dict.new()) } +/// 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#Element)) +/// and only reach for components when you really need to encapsulate that update +/// loop. /// pub fn component( init: fn(flags) -> #(model, Effect(msg)), @@ -217,13 +344,23 @@ pub fn component( // 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 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(ClientSpa, msg)) -> Nil, Error) { +) -> Result(fn(Action(msg, ClientSpa)) -> Nil, Error) { use <- bool.guard(!is_browser(), Error(NotABrowser)) do_start(app, selector, flags) } @@ -233,7 +370,7 @@ fn do_start( _app: App(flags, model, msg), _selector: String, _flags: flags, -) -> Result(fn(Action(ClientSpa, msg)) -> Nil, Error) { +) -> 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 @@ -241,28 +378,42 @@ fn do_start( 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, "./server-runtime.ffi.mjs", "start") pub fn start_server_component( app: App(flags, model, msg), with flags: flags, -) -> Result(fn(Action(ServerComponent, msg)) -> Nil, Error) { +) -> 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) /// /// -/// 🚨 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. +/// **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(ServerComponent, msg)), Error) { +) -> Result(Subject(Action(msg, ServerComponent)), Error) { do_start_actor(app, flags) } @@ -275,7 +426,7 @@ fn do_start_actor(_, _) { fn do_start_actor( app: App(flags, model, msg), flags: flags, -) -> Result(Subject(Action(ServerComponent, msg)), Error) { +) -> Result(Subject(Action(msg, ServerComponent)), Error) { app.init(flags) |> runtime.start(app.update, app.view, app.on_attribute_change) |> result.map_error(ActorError) @@ -283,47 +434,71 @@ fn do_start_actor( /// 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. +/// 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 provided application can only have `Nil` flags, because there is no way -/// to specify flags when the component is first rendered. +/// 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", [], [])`. /// -/// 💡 There are [some rules](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names) +/// **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. /// -/// 🚨 This function is only meaningful when running in the browser. For server -/// contexts, you can render a Lustre Server Component using `start_server_component` -/// or `start_actor` instead. +/// **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, "./client-component.ffi.mjs", "register") -pub fn register(app: App(Nil, model, msg), name: String) -> Result(Nil, Error) { +pub fn register(_app: App(Nil, model, msg), _name: String) -> Result(Nil, Error) { Error(NotABrowser) } // ACTIONS --------------------------------------------------------------------- +/// A [`ServerComponent`](#ServerComponent) broadcasts patches to be applied to +/// the DOM to any connected clients. This action is used to add a new client to +/// a running server component. +/// +/// The `id` should be a unique identifier for the client, but it can be any type +/// you want. This is only used if you want to remove the client in the future +/// using [`remove_renderer`](#remove_renderer). +/// pub fn add_renderer( id: any, renderer: fn(Patch(msg)) -> Nil, -) -> Action(ServerComponent, msg) { +) -> Action(msg, ServerComponent) { runtime.AddRenderer(dynamic.from(id), renderer) } -pub fn dispatch(msg: msg) -> Action(runtime, msg) { +/// 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) } -pub fn event(name: String, data: Dynamic) -> Action(ServerComponent, msg) { - runtime.Event(name, data) -} - -pub fn remove_renderer(id: any) -> Action(ServerComponent, msg) { +/// Remove a registered renderer from a server component. If no renderer with the +/// given id is found, this action has no effect. +/// +pub fn remove_renderer(id: any) -> Action(msg, ServerComponent) { runtime.RemoveRenderer(dynamic.from(id)) } -pub fn shutdown() -> Action(runtime, 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 } @@ -333,7 +508,7 @@ pub fn shutdown() -> Action(runtime, msg) { /// of a function for different targets, but it's not possible to know what runtime /// you're targetting at compile-time. /// -/// This is problematic if you're using Lustre Server Components with a JavaScript +/// 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! /// @@ -347,6 +522,6 @@ pub fn is_browser() -> Bool { /// may have been registered and you must avoid collisions. /// @external(javascript, "./client-runtime.ffi.mjs", "is_registered") -pub fn is_registered(name: String) -> Bool { +pub fn is_registered(_name: String) -> Bool { False } |