aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2024-02-11 16:50:25 +0000
committerHayleigh Thompson <me@hayleigh.dev>2024-02-11 16:50:25 +0000
commitc4e9f6a2708940f692066a2a28e6f766c454b80d (patch)
tree6bb38b90c067a1f372aa9c6d1d626dba2809c339
parent978e56bf9b2f0258c341fb7f3a0fcbd2d3a3ac4a (diff)
downloadlustre-c4e9f6a2708940f692066a2a28e6f766c454b80d.tar.gz
lustre-c4e9f6a2708940f692066a2a28e6f766c454b80d.zip
:memo: Flesh out API docs.
-rw-r--r--src/lustre.gleam285
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
}