aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/07-routing/README.md56
-rw-r--r--examples/07-routing/header.pngbin93127 -> 317512 bytes
-rw-r--r--examples/07-routing/src/app.gleam98
3 files changed, 104 insertions, 50 deletions
diff --git a/examples/07-routing/README.md b/examples/07-routing/README.md
index 5eaf0cd..217b674 100644
--- a/examples/07-routing/README.md
+++ b/examples/07-routing/README.md
@@ -2,12 +2,62 @@
# 07 Routing
-We haven't quite got round to documenting this example yet. You can still check out the [source code](./src/app.gleam) and run it with the following command:
+In this example, we demonstrate basic routing using the community library [modem](https://hexdocs.pm/modem/). Modem's quickstart docs should be all you should need to get up to speed, so that's the best place to start.
-```sh
-gleam run -m lustre/dev start --use-example-styles
+Of course, it's not much fun routing without something to route _to_. This example lets users create new pages on the fly - a guest book for your next house party! Hospitality is very important, and guests will be sure to feel welcome when they see their name in the navigation with a special greeting page just for them.
+
+## Using Modem
+
+Modem uses [custom side effects](../06-custom-effects/) and external Javascript to translate browser click and navigation events into `update` messages for the Lustre runtime. All we need to use it is a [route change handler function](./src/app.gleam#L59) that we can pass to `modem.init` in our app's `init` function.
+
+> _Note:_ See [`modem.advanced`](https://hexdocs.pm/modem/modem.html#advanced) to configure more options.
+
+Inside our `on_route_change` function, we match URI path segment patterns to our routes:
+
+```gleam
+ fn on_route_change(uri: Uri) -> Msg {
+ case uri.path_segments(uri.path) {
+ ["welcome", guest] -> OnRouteChange(WelcomeGuest(guest))
+ _ -> OnRouteChange(Home)
+ }
+ }
+```
+
+In our `update` function, we assign the matched route to `model.current_route`:
+
+```gleam
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ case msg {
+ OnRouteChange(route) -> #(
+ Model(..model, current_route: route),
+ effect.none(),
+ )
+ ...
+ }
```
+And in our `view` function, we use `model.current_route` to determine what to display to the user:
+
+```gleam
+fn view(model: Model) -> Element(Msg) {
+ let page = case model.current_route {
+ Home -> render_home(model)
+ WelcomeGuest(name) -> render_welcome(model, name)
+ }
+ ...
+}
+```
+
+## Views: They're just functions!
+
+Lustre doesn't provide a traditional HTML or JSX-style templating engine, and this is by design.
+
+Since the `view` portion of this example is a bit more involved than our previous ones have been, it should start to give you more of a feel for how views in Lustre are _just functions_. The layout is a function. Each page view is a function. The nav is a function, and each individual nav _item_ is a function too.
+
+This means we can build up our entire UI using Gleam's functional syntax, benefiting from features like exhaustive pattern matching based on our routes.
+
+Since our views are [pure functions](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md), we know they'll always render reliably.
+
## Getting help
If you're having trouble with Lustre or are not sure what the right way to do
diff --git a/examples/07-routing/header.png b/examples/07-routing/header.png
index ffee3c6..f903c89 100644
--- a/examples/07-routing/header.png
+++ b/examples/07-routing/header.png
Binary files differ
diff --git a/examples/07-routing/src/app.gleam b/examples/07-routing/src/app.gleam
index 382bd60..e82f895 100644
--- a/examples/07-routing/src/app.gleam
+++ b/examples/07-routing/src/app.gleam
@@ -19,6 +19,8 @@ import modem
// In your own apps, make sure to add the `lustre/ui` dependency and include the
// stylesheet somewhere.
import lustre/ui
+import lustre/ui/layout/cluster
+import lustre/ui/util/cn
// MAIN ------------------------------------------------------------------------
@@ -30,7 +32,7 @@ pub fn main() {
// MODEL -----------------------------------------------------------------------
type Model {
- Model(current: Route, guests: List(Guest), new_guest_name: String)
+ Model(current_route: Route, guests: List(Guest), new_guest_name: String)
}
type Route {
@@ -45,7 +47,7 @@ type Guest {
fn init(_) -> #(Model, Effect(Msg)) {
#(
Model(
- current: Home,
+ current_route: Home,
guests: [
Guest(slug: "chihiro", name: "Chihiro"),
Guest(slug: "totoro", name: "Totoro"),
@@ -73,7 +75,10 @@ pub opaque type Msg {
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
- OnRouteChange(route) -> #(Model(..model, current: route), effect.none())
+ OnRouteChange(route) -> #(
+ Model(..model, current_route: route),
+ effect.none(),
+ )
UserUpdatedNewGuestName(name) -> #(
Model(..model, new_guest_name: name),
effect.none(),
@@ -94,36 +99,18 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
fn view(model: Model) -> Element(Msg) {
let styles = [#("margin", "15vh")]
- let page = case model.current {
- WelcomeGuest(name) -> render_welcome(model, name)
- Home -> render_home(model)
- }
-
- ui.stack([attribute.style(styles)], [render_nav(model), page])
-}
-
-fn render_nav(model: Model) -> Element(a) {
- let item_styles = [#("margin", "15px"), #("text-decoration", "underline")]
-
- let nav_item = fn(path, text) {
- html.a([attribute.href("/" <> path), attribute.style(item_styles)], [
- element.text(text),
- ])
+ let page = case model.current_route {
+ Home -> view_home(model)
+ WelcomeGuest(name) -> view_welcome(model, name)
}
- let guests =
- model.guests
- |> list.map(fn(guest: Guest) {
- nav_item("welcome/" <> guest.slug, guest.name)
- })
-
- html.nav([], [nav_item("", "Home"), ..guests])
+ ui.stack([attribute.style(styles)], [view_nav(model), page])
}
-fn render_home(model: Model) {
+fn view_home(model: Model) {
let new_guest_input = fn(event) {
- use code <- result.try(dynamic.field("key", dynamic.string)(event))
- case code {
+ use key_code <- result.try(dynamic.field("key", dynamic.string)(event))
+ case key_code {
"Enter" -> {
let guest_slug =
model.new_guest_name
@@ -139,35 +126,52 @@ fn render_home(model: Model) {
}
}
}
- ui.centre(
- [attribute.style([#("margin-top", "10vh")])],
- ui.stack([], [
- to_title("Welcome to the Party 🏡"),
- html.label([], [element.text("Please sign the guest book:")]),
- ui.input([
- event.on("keyup", new_guest_input),
- attribute.value(model.new_guest_name),
- ]),
+
+ view_body([
+ view_title("Welcome to the Party 🏡"),
+ html.p([], [element.text("Please sign the guest book:")]),
+ ui.input([
+ event.on("keyup", new_guest_input),
+ attribute.value(model.new_guest_name),
]),
- )
+ ])
}
-fn render_welcome(model: Model, slug) -> Element(a) {
+fn view_welcome(model: Model, slug) -> Element(a) {
let guest =
model.guests
|> list.find(fn(guest: Guest) { guest.slug == slug })
let title = case guest {
- Ok(guest) -> to_title("Hello, " <> guest.name <> "! 🎉")
- _ -> to_title("Sorry ... didn't quite catch that.")
+ Ok(guest) -> view_title("Hello, " <> guest.name <> "! 🎉")
+ _ -> view_title("Sorry ... didn't quite catch that.")
}
- ui.centre([attribute.style([#("margin-top", "10vh")])], title)
+ view_body([title])
}
-fn to_title(text) {
- html.h1(
- [attribute.style([#("font-size", "36px"), #("font-weight", "bold")])],
- [element.text(text)],
- )
+fn view_nav(model: Model) -> Element(a) {
+ let item_styles = [#("text-decoration", "underline")]
+
+ let view_nav_item = fn(path, text) {
+ html.a([attribute.href("/" <> path), attribute.style(item_styles)], [
+ element.text(text),
+ ])
+ }
+
+ let guest_nav_items =
+ model.guests
+ |> list.map(fn(guest: Guest) {
+ view_nav_item("welcome/" <> guest.slug, guest.name)
+ })
+
+ cluster.of(html.nav, [], [view_nav_item("", "Home"), ..guest_nav_items])
+}
+
+fn view_body(children) {
+ ui.centre([cn.mt_xl()], ui.stack([], children))
+}
+
+fn view_title(text) {
+ html.h1([cn.text_xl()], [element.text(text)])
}