diff options
Diffstat (limited to 'examples')
-rw-r--r-- | examples/07-routing/README.md | 56 | ||||
-rw-r--r-- | examples/07-routing/header.png | bin | 93127 -> 317512 bytes | |||
-rw-r--r-- | examples/07-routing/src/app.gleam | 98 |
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 Binary files differindex ffee3c6..f903c89 100644 --- a/examples/07-routing/header.png +++ b/examples/07-routing/header.png 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)]) } |