diff options
Diffstat (limited to 'examples')
-rw-r--r-- | examples/03-controlled-inputs/README.md | 21 | ||||
-rw-r--r-- | examples/03-controlled-inputs/src/app.gleam | 15 | ||||
-rw-r--r-- | examples/04-custom-event-handlers/README.md | 4 | ||||
-rw-r--r-- | examples/04-custom-event-handlers/src/app.gleam | 12 | ||||
-rw-r--r-- | examples/05-http-requests/README.md | 74 | ||||
-rw-r--r-- | examples/05-http-requests/src/app.gleam | 16 | ||||
-rw-r--r-- | examples/06-custom-effects/README.md | 12 | ||||
-rw-r--r-- | examples/06-custom-effects/src/app.gleam | 24 |
8 files changed, 122 insertions, 56 deletions
diff --git a/examples/03-controlled-inputs/README.md b/examples/03-controlled-inputs/README.md index 30fda75..95ca620 100644 --- a/examples/03-controlled-inputs/README.md +++ b/examples/03-controlled-inputs/README.md @@ -18,8 +18,9 @@ two things: ui.input([ // Input's value is fixed to the model's `value` field attribute.value(model.value), - // Whenever the input changes, we send a `GotInput` message with the new value - event.on_input(GotInput) + // Whenever the input changes, we send a `UserUpdatedMessage` message with the + // new value + event.on_input(UserUpdatedMessage) ]) ``` @@ -36,7 +37,7 @@ value is less than 10 characters long. ```gleam case msg { - GotInput(value) -> { + UserUpdatedMessage(value) -> { let length = string.length(value) case length <= model.max { @@ -48,6 +49,20 @@ case msg { ... ``` +## A note on message naming + +In our [state management guide](https://hexdocs.pm/lustre/guide/02-state-management.html) +we touch on the idea of "messages not actions." We think the best way to name your +messages is following a "Subject Verb Object" pattern: `UserUpdatedMessage` not +`SetMessage` and so on. + +This approach to message naming can feel a cumbersome at first, especially for +small examples like this. One of Lustre's super powers is that as your app grows +in size, your `Msg` type becomes a very helpful overview of all the different +events your app can handle. When they take the form of `Subject Verb Object` it +gives you an immediate sense of the different things that speak to your app: how +much is coming from your backend, how much is user input, and so on. + ## Getting help If you're having trouble with Lustre or not sure what the right way to do diff --git a/examples/03-controlled-inputs/src/app.gleam b/examples/03-controlled-inputs/src/app.gleam index d3c764e..f620e6a 100644 --- a/examples/03-controlled-inputs/src/app.gleam +++ b/examples/03-controlled-inputs/src/app.gleam @@ -35,13 +35,13 @@ fn init(_) -> Model { // UPDATE ---------------------------------------------------------------------- pub opaque type Msg { - GotInput(value: String) - Reset + UserUpdatedMessage(value: String) + UserResetMessage } fn update(model: Model, msg: Msg) -> Model { case msg { - GotInput(value) -> { + UserUpdatedMessage(value) -> { let length = string.length(value) case length <= model.max { @@ -49,7 +49,7 @@ fn update(model: Model, msg: Msg) -> Model { False -> model } } - Reset -> Model(..model, value: "", length: 0) + UserResetMessage -> Model(..model, value: "", length: 0) } } @@ -67,10 +67,13 @@ fn view(model: Model) -> Element(Msg) { ui.field( [], [element.text("Write a message:")], - ui.input([attribute.value(model.value), event.on_input(GotInput)]), + ui.input([ + attribute.value(model.value), + event.on_input(UserUpdatedMessage), + ]), [element.text(length <> "/" <> max)], ), - ui.button([event.on_click(Reset)], [element.text("Reset")]), + ui.button([event.on_click(UserResetMessage)], [element.text("Reset")]), ), ) } diff --git a/examples/04-custom-event-handlers/README.md b/examples/04-custom-event-handlers/README.md index a2a09c5..434725a 100644 --- a/examples/04-custom-event-handlers/README.md +++ b/examples/04-custom-event-handlers/README.md @@ -74,7 +74,3 @@ In this [example code](./src/app.gleam#L63), we define a custom input handler ca 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/examples/04-custom-event-handlers/src/app.gleam b/examples/04-custom-event-handlers/src/app.gleam index c3f5d0b..011b09c 100644 --- a/examples/04-custom-event-handlers/src/app.gleam +++ b/examples/04-custom-event-handlers/src/app.gleam @@ -37,20 +37,20 @@ fn init(_) -> Model { // UPDATE ---------------------------------------------------------------------- pub opaque type Msg { - GotInput(value: String) - Reset + UserUpdatedMessage(value: String) + UserResetMessage } fn update(model: Model, msg: Msg) -> Model { case msg { - GotInput(value) -> { + UserUpdatedMessage(value) -> { let length = string.length(value) case length <= model.max { True -> Model(..model, value: value, length: length) False -> model } } - Reset -> Model(..model, value: "", length: 0) + UserResetMessage -> Model(..model, value: "", length: 0) } } @@ -66,7 +66,7 @@ fn view(model: Model) -> Element(Msg) { let loud = string.uppercase(value) - Ok(GotInput(loud)) + Ok(UserUpdatedMessage(loud)) } ui.centre( @@ -79,7 +79,7 @@ fn view(model: Model) -> Element(Msg) { ui.input([attribute.value(model.value), event.on("input", make_it_loud)]), [element.text(length <> "/" <> max)], ), - ui.button([event.on_click(Reset)], [element.text("Reset")]), + ui.button([event.on_click(UserResetMessage)], [element.text("Reset")]), ), ) } diff --git a/examples/05-http-requests/README.md b/examples/05-http-requests/README.md index 3aacfe5..4a118e4 100644 --- a/examples/05-http-requests/README.md +++ b/examples/05-http-requests/README.md @@ -2,25 +2,28 @@ # 05 HTTP Requests -Up until now, all the logic in our examples has run neatly in a self-contained `Init -> Update 🔁 View` loop. But our applications often need to interact with the outside world, whether through browser APIs or HTTP requests. - -Up until now, we've seen Lustre applications constructed with the `lustre.simple` -constructor. These kinds of applications are great for introducing the Model-View-Update -(MVU) pattern, but for most real-world applications we'll need a way to talk to -the outside world. +In the previous examples, we've seen Lustre applications constructed with the +[`lustre.simple`](https://hexdocs.pm/lustre/lustre.html#simple) constructor. +These kinds of applications are great for introducing the Model-View-Update (MVU) +pattern, but for most real-world applications we'll need a way to talk to the +outside world. Lustre's runtime includes _managed effects_, which allow us to perform side effects like HTTP requests and communicate the results back to our application's `update` function. To learn more about Lustre's effect system and why it's useful, check out the [side effects guide](https://hexdocs.pm/lustre/guide/side-effects.html), -or the docs for the [lustre/effect module](https://hexdocs.pm/lustre/lustre/effect.html) -For now, we will focus on how to send HTTP requests in a Lustre application: a -pretty important thing to know! +or the docs for the [`lustre/effect` module](https://hexdocs.pm/lustre/lustre/effect.html). + +This example is a practical look at what effects mean in Lustre, and we'll look +at how to send HTTP requests in a Lustre application: a pretty important thing to +know! ## Moving on from `lustre.simple` -From now on, the rest of these examples will use a different application constructor: -[`lustre.application`]. Let's compare the type of both functions: +From this example onwards, we will use a new application constructor: +[`lustre.application`](https://hexdocs.pm/lustre/lustre.html#application). Full Lustre +applications have the ability to communicate to the runtime. Let's compare the type +of both the `simple` and `application` functions: ```gleam pub fn simple( @@ -70,7 +73,7 @@ fn get_quote() -> Effect(Msg) { dynamic.field("content", dynamic.string), ) - lustre_http.get(url, lustre_http.expect_json(decoder, GotQuote)) + lustre_http.get(url, lustre_http.expect_json(decoder, ApiUpdatedQuote)) } ``` @@ -80,20 +83,55 @@ To construct HTTP requests, we need a few different things: - A description of what we _expect_ the result to be. There are a few options: `expect_anything`, `expect_text`, `expect_json`. In this example we say we're - expecting a JSON response and provide a decode. + expecting a JSON response and provide a decoder. -- A long with what we expect the response to be, we also need to provide a way +- Along with what we expect the response to be, we also need to provide a way to turn that response into a `Msg` value that our `update` function can handle. The same applies for post requests too, but there you also need to provide the JSON body of the request. +## Tying it together + +We now have a function that can create an `Effect` for us, but we need to hand it +to the runtime to be executed. The only way we can do that is by returning it from +our `update` (or `init`) function! We attach an event listener on a button, and +when the user clicks that button we'll return the `Effect` we want to perform as +the second element of a tuple: + +```gleam +fn view(model: Model) -> Element(Msg) { + ui.centre([], + ui.button([event.on_click(UserClickedRefresh)], [ + element.text("New quote"), + ]), + ) +} + +fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { + case msg { + UserClickedRefresh -> #(model, get_quote()) + ... + } +} +``` + +Of course, we need to handle responses from the quote API in our `update` function +too. When there are no side effects we want the runtime to perform for us, we need +to call `effect.none()`: + +```gleam +fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { + case msg { + ... + ApiUpdatedQuote(Ok(quote)) -> #(Model(quote: Some(quote)), effect.none()) + ApiUpdatedQuote(Error(_)) -> #(model, effect.none()) + } +} +``` + ## 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/examples/05-http-requests/src/app.gleam b/examples/05-http-requests/src/app.gleam index 04e0af6..9442cf3 100644 --- a/examples/05-http-requests/src/app.gleam +++ b/examples/05-http-requests/src/app.gleam @@ -45,15 +45,15 @@ fn init(_) -> #(Model, Effect(Msg)) { // UPDATE ---------------------------------------------------------------------- pub opaque type Msg { - Refresh - GotQuote(Result(Quote, HttpError)) + UserClickedRefresh + ApiUpdatedQuote(Result(Quote, HttpError)) } fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { case msg { - Refresh -> #(model, get_quote()) - GotQuote(Ok(quote)) -> #(Model(quote: Some(quote)), effect.none()) - GotQuote(Error(_)) -> #(model, effect.none()) + UserClickedRefresh -> #(model, get_quote()) + ApiUpdatedQuote(Ok(quote)) -> #(Model(quote: Some(quote)), effect.none()) + ApiUpdatedQuote(Error(_)) -> #(model, effect.none()) } } @@ -66,7 +66,7 @@ fn get_quote() -> Effect(Msg) { dynamic.field("content", dynamic.string), ) - lustre_http.get(url, lustre_http.expect_json(decoder, GotQuote)) + lustre_http.get(url, lustre_http.expect_json(decoder, ApiUpdatedQuote)) } // VIEW ------------------------------------------------------------------------ @@ -79,7 +79,9 @@ fn view(model: Model) -> Element(Msg) { ui.aside( [aside.min_width(70), attribute.style([#("width", "60ch")])], view_quote(model.quote), - ui.button([event.on_click(Refresh)], [element.text("New quote")]), + ui.button([event.on_click(UserClickedRefresh)], [ + element.text("New quote"), + ]), ), ) } diff --git a/examples/06-custom-effects/README.md b/examples/06-custom-effects/README.md index f8ad49f..fe6822b 100644 --- a/examples/06-custom-effects/README.md +++ b/examples/06-custom-effects/README.md @@ -5,6 +5,18 @@ bit about Lustre or Elm and want to help out, we'd love to have your help! Pleas [open an issue](https://github.com/lustre-labs/lustre/issues/new) if you have any ideas or reach out to @hayleigh.dev on the [Gleam discord](https://discord.gg/Fm8Pwmy). +## Another note on message naming + +In our [controlled inputs example](https://github.com/lustre-labs/lustre/tree/main/examples/03-controlled-inputs) +we touched on the idea of naming messages in a "Subject Verb Object" pattern. This +example neatly shows the benefits of taking such an approach once different "things" +start talking to your application. + +It would be easy to have a single `SetMessage` variant that both the user input +and local storage lookup use to update the model, but doing so might encourage +us to conceal the fact that the local storage lookup can fail and makes it harder +to see what things our app deals with. + ## Getting help If you're having trouble with Lustre or not sure what the right way to do diff --git a/examples/06-custom-effects/src/app.gleam b/examples/06-custom-effects/src/app.gleam index 5399903..e04484a 100644 --- a/examples/06-custom-effects/src/app.gleam +++ b/examples/06-custom-effects/src/app.gleam @@ -28,34 +28,34 @@ type Model { } fn init(_) -> #(Model, Effect(Msg)) { - #(Model(message: None), read_localstorage("message", GotMessage)) + #(Model(message: None), read_localstorage("message")) } // UPDATE ---------------------------------------------------------------------- pub opaque type Msg { - GotInput(String) - GotMessage(Result(String, Nil)) + UserUpdatedMessage(String) + CacheUpdatedMessage(Result(String, Nil)) } fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { case msg { - GotInput(input) -> #( + UserUpdatedMessage(input) -> #( Model(message: Some(input)), write_localstorage("message", input), ) - GotMessage(Ok(message)) -> #(Model(message: Some(message)), effect.none()) - GotMessage(Error(_)) -> #(model, effect.none()) + CacheUpdatedMessage(Ok(message)) -> #( + Model(message: Some(message)), + effect.none(), + ) + CacheUpdatedMessage(Error(_)) -> #(model, effect.none()) } } -fn read_localstorage( - key: String, - to_msg: fn(Result(String, Nil)) -> msg, -) -> Effect(msg) { +fn read_localstorage(key: String) -> Effect(Msg) { effect.from(fn(dispatch) { do_read_localstorage(key) - |> to_msg + |> CacheUpdatedMessage |> dispatch }) } @@ -85,7 +85,7 @@ fn view(model: Model) -> Element(Msg) { ui.field( [], [], - ui.input([attribute.value(message), event.on_input(GotInput)]), + ui.input([attribute.value(message), event.on_input(UserUpdatedMessage)]), [element.text("Type a message and refresh the page")], ), ) |