diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2024-02-21 13:57:53 +0000 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2024-02-21 13:57:53 +0000 |
commit | b562382e7b4f9a4809863d4fe9a32a36467dbac9 (patch) | |
tree | 28fe47870989e1449959a58990f91834d9588e6e | |
parent | c15fa1096df2429935932d0e1fb7eb20702a0e37 (diff) | |
download | lustre-b562382e7b4f9a4809863d4fe9a32a36467dbac9.tar.gz lustre-b562382e7b4f9a4809863d4fe9a32a36467dbac9.zip |
:memo: Write quickstart guide.
-rw-r--r-- | docs/guide/01-quickstart.md | 370 | ||||
-rw-r--r-- | src/lustre.gleam | 2 |
2 files changed, 371 insertions, 1 deletions
diff --git a/docs/guide/01-quickstart.md b/docs/guide/01-quickstart.md new file mode 100644 index 0000000..c18d3bf --- /dev/null +++ b/docs/guide/01-quickstart.md @@ -0,0 +1,370 @@ +# 01 Quickstart Guide + +Welcome to the Lustre quickstart guide! This document should get you up to speed +with the core ideas that underpin every Lustre application as well as how to get +something on the screen. + +## What is a SPA? + +Lustre can be used to create HTML in many different contexts, but it is primarily +designed to be used to build Single-Page Applications – or SPAs. SPAs are a type +of Web application that render content primarily in the browser (rather than on +the server) and, crucially, do not require a full page load when navigating +between pages or loading new content. + +To help build these kinds of applications, Lustre comes with an opinionated +runtime. Some of Lustre's core features include: + +- **Declarative rendering**: User interfaces are constructed using a declarative + API that describes HTML as a function of your application's state. This is in + contrast to more traditional imperative approaches to direct DOM mutation like + jQuery. + +- **State management**: If UIs are a function of state, then orchestrating state + changes is crucial! Lustre provides a simple message-based state management + system modelled after OTP [gen_servers](https://www.erlang.org/doc/design_principles/gen_server_concepts), + Gleam's [actors](https://hexdocs.pm/gleam_otp/gleam/otp/actor.html), and the + [Elm Architecture](https://guide.elm-lang.org/architecture/). + +- **Managed side effects**: Managing asynchronous operations like HTTP requests + and timers can be tricky when JavaScript is single-threaded. Lustre provides a + runtime to manage these side effects and let them communicate with your application + using the same messages as your update loop. + +## Your first Lustre program + +To get started, let's create a new Gleam application and add Lustre as a dependency. + +```sh +$ gleam new app && gleam add lustre +``` + +By default, Gleam builds projects for the Erlang target unless told otherwise. We +can change this by adding a `target` field to the `gleam.toml` file generated in +the root of the project. + +```toml +name = "app" +target = "javascript" +version = "1.0.0" + +... +``` + +The simplest type of Lustre application is constructed with the `element` function. +This produces an application that renders a static piece of content without the +typical update loop. + +We can start by importing `lustre` and `lustre/element` and just rendering some +text: + +```gleam +import lustre +import lustre/element + +pub fn main() { + lustre.element(element.text("Hello, world!")) +} +``` + +Lustre includes tooling like a server to serve your application in development. +You can start that server by running: + +```sh +$ gleam run -m lustre dev +``` + +The first time you run this command might take a little while, but subsequent runs +should be much faster! + +> **Note**: Lustre uses esbuild under the hood, and attempts to download the [right +> binary for your platform](https://esbuild.github.io/getting-started/#download-a-build). +> If you're not connected to the internet, on an unsupported platform, or don't +> want Lustre to download the binary you can grab or build it yourself and place it +> in `build/.lustre/bin/esbuild`. + +Once the server is up and running you should be able to visit http://localhost:1234 +and be greeted with your "Hello, world!" message. + +We mentioned Lustre has a declarative API for constructing HTML. Let's see what +that looks like by building something slightly more complex. + +```gleam +import lustre +import lustre/attribute +import lustre/element +import lustre/element/html + +pub fn main() { + lustre.element( + html.div([], [ + html.h1([], [element.text("Hello, world!")]), + html.figure([], [ + html.img([attribute.src("https://cataas.com/cat")]) + html.figcaption([], [element.text("A cat!")]) + ]) + ]) + ) +} +``` + +Here we _describe_ the structure of the HTML we want to render, and leave the +busywork to Lustre's runtime: that's what makes it declarative! + +"**Where are the templates?**" we hear you cry. Lustre doesn't have a separate +templating syntax like JSX or HEEx for a few reasons (lack of metaprogramming +built into Gleam, for one). Some folks might find this a bit odd at first, but +we encourage you to give it a try. Realising that your UI is _just functions_ +can be a bit of a lightbulb moment as you build more complex applications. + +## Adding interactivity + +Rendering static HTML is great, but we said at the beginning Lustre was designed +primarily for building SPAs – and SPAs are interactive! To do that we'll need +to move on from `lustre.element` to the first of Lustre's application constructors +that includes an update loop: `lustre.simple`. + +```gleam +import gleam/int +import lustre +import lustre/element +import lustre/element/html +import lustre/event + +pub fn main() { + lustre.simple(init, update, view) +} +``` + +There are three main building blocks to every interactive Lustre application: + +- 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. + +We'll build a simple counter application to demonstrate these concepts. Our +model can be an `Int` and our `init` function will initialise it to `0`: + +```gleam +pub type Model = Int + +fn init(_) -> Model { + 0 +} +``` + +> **Note**: The `init` function always takes a single argument! These are the "flags" +> or start arguments you can pass in when your application is started with +> `lustre.start`. For the time being, we can ignore them, but they're useful for +> passing in configuration or other data when your application starts. + +The main update loop in a Lustre application revolves around messages passed in +from the outside world. For our counter application, we'll have two messages to +increment and decrement the counter: + +```gleam +pub type Msg { + Increment + Decrement +} + +pub fn update(model: Model, msg: Msg) -> Model { + case msg { + Increment -> model + 1 + Decrement -> model - 1 + } +} +``` + +Each time a message is produced from an event listener, Lustre will call your +`update` function with the current model and the incoming message. The result +will be the new application state that is then passed to the `view` function: + +```gleam +pub fn view(model: Model) -> lustre.Element(Msg) { + let count = int.to_string(model) + + html.div([], [ + html.button([event.click(Increment)], [ + element.text("+") + ]), + element.text(count), + html.button([event.click(Decrement)], [ + element.text("-") + ]) + ]) +} +``` + +This forms the core of every Lustre application: + +- A model produces some view. +- The view can produce messages in response to user interaction. +- Those messages are passed to the update function to produce a new model. +- ... and the cycle continues. + +## Talking to the outside world + +This "closed loop" of messages and updates works well if all we need is an +interactive document, but many applications will also need to talk to the outside +world – whether that's fetching data from an API, setting up a WebSocket connection, +or even just setting a timer. + +Lustre manages these side effects through an abstraction called an `Effect`. In +essense, effects are any functions that talk with the outside world and might +want to send messages back to your application. Lustre lets you write your own +effects, but for now we'll use a community package called +[`lustre_http`](https://hexdocs.pm/lustre_http/index.html) to fetch a new cat image +every time the counter is incremented. + +Because this is a separate package, make sure to add it to your project first. +While we're here, we'll also add `gleam_json` so we can decode the response from +the cat API: + +```sh +$ gleam add gleam_json lustre_http +``` + +Now we are introducing side effects, we need to graduate from `lustre.simple` to +the more powerful `lustre.application` constructor. + +```gleam +import gleam/int +import lustre +import lustre_http +import lustre/attribute +import lustre/element +import lustre/element/html +import lustre/event + +pub fn main() { + lustre.application(init, update, view) +} +``` + +If you edited your previous counter app, you'll notice the prorgam no longer +compiles. Specifically, the type of our `init` and `update` functions are wrong +for the new `lustre.application` constructor! + +In order to tell Lustre about what effects it should perform, these functions now +need to return a _tuple_ of the new model and any effects. We can amend our `init` +function like so: + +```gleam +pub type Model { + Model(count: Int, cats: List(String)) +} + +fn init(_) -> #(Model, effect.Effect(Msg)) { + #(Model(0, []), effect.none()) +} +``` + +The `effect.none` function is a way of saying "no effects" – we don't need to do +anything when the application starts. We've also changed our `Model` type from a +simple type alias to a Gleam [record](https://tour.gleam.run/data-types/records/) +that holds both the current count and a list of cat image URLs. + +In our `update` function, we want to fetch a new cat image every time the counter +is incremented. To do this we need two things: + +- An `Effect` to describe the request the runtime should perform. +- A variant of our `Msg` to handle the response. + +The `lustre_http` package has the effect side of things handled, so we just need +to modify our `Msg` type to include a new variant for the response: + +```gleam +pub type Msg { + Increment + Decrement + GotCat(Result(String, lustre_http.HttpError)) +} +``` + +Finally, we can modify our `update` function to also fetch a cat image when the +counter is incremeneted and handle the response: + +```gleam +pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { + case msg { + Increment -> #(Model(..model, count: model.count + 1), get_cat()) + Decrement -> #(Model(..model, count: model.count - 1), effect.none()) + GotCat(Ok(cat)) -> #(Model(..model, [cat, ..model.cats]), effect.none()) + GotCat(Error(_)) -> #(model, effect.none()) + } +} + +fn get_cat() -> effect.Effect(Msg) { + let decoder = dynamic.field("_id", dynamic.string) + let expect = lustre_http.expect_json(decoder, GotCat) + + lustre_http.get("https://cataas.com/cat?json=true", expect) +} +``` + +This model of managed effects can feel cumbersome at first, but it comes with some +benefits. Forcing side effects to produce a message means our message type naturally +describes all the ways the world can communicate with our application; as an app +grows being able to get this kind of overview is invaluable! It also means we can +test our update loop in isolation from the runtime and side effects: we could write +tests that verify a particular sequence of messages produces an expected model +without needing to mock out HTTP requests or timers. + +Before we forget, let's also update our `view` function to actually display the +cat images we're fetching: + +```gleam +pub fn view(model: Model) -> lustre.Element(Msg) { + let count = int.to_string(model.count) + + html.div([], [ + html.button([event.click(Increment)], [ + element.text("+") + ]), + element.text(count), + html.button([event.click(Decrement)], [ + element.text("-") + ]), + html.div([], list.map(model.cats, fn(cat) { + html.img([attribute.src(cat)]) + })) + ]) +} +``` + +## Where to go from here + +Believe it or not, you've already seen about 80% of what Lustre has to offer! From +these core concepts, you can build rich interactive applications that are predictable +and maintainable. Where to go from here depends on what you want to build, and +how you like to learn: + +- There are a number of [examples](https://github.com/lustre-labs/lustre/tree/main/examples) + if the Lustre repository that gradually introduce more complex applications + and ideas. + +- The [rest of this guide](/guide/02-state-management) also continues to teach + Lustre's high-level concepts and best-practices. + +- If you're coming from LiveView or have heard about Lustre's server components + and want to learn more, you can skip to the [server components](/guide/05-server-components) + section of the guide to learn about how to run Lustre applications on the backend. + +- Of course, if you want to dive in and start making things straight away, the + [API documentation](/lustre) is always handy to keep open. + +## 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. diff --git a/src/lustre.gleam b/src/lustre.gleam index 7cb6d3f..a2c2fe2 100644 --- a/src/lustre.gleam +++ b/src/lustre.gleam @@ -90,7 +90,7 @@ //// 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) //// //// 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! |