mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-14 00:27:12 +00:00
docs: (in-progress) new tutorial/guide format with integrated CodeSandboxes (#375)
This commit is contained in:
parent
256cf0c59b
commit
44c18da324
13 changed files with 1557 additions and 5 deletions
|
@ -15,11 +15,6 @@ members = [
|
||||||
# libraries
|
# libraries
|
||||||
"meta",
|
"meta",
|
||||||
"router",
|
"router",
|
||||||
|
|
||||||
# book
|
|
||||||
"docs/book/project/ch02_getting_started",
|
|
||||||
"docs/book/project/ch03_building_ui",
|
|
||||||
"docs/book/project/ch04_reactivity",
|
|
||||||
]
|
]
|
||||||
exclude = ["benchmarks", "examples"]
|
exclude = ["benchmarks", "examples"]
|
||||||
|
|
||||||
|
|
20
docs/book/src/01_introduction.md
Normal file
20
docs/book/src/01_introduction.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
|
||||||
|
It will walk through the fundamental concepts you need to build applications,
|
||||||
|
beginning with a simple application rendered in the browser, and building toward a
|
||||||
|
full-stack application with server-side rendering and hydration.
|
||||||
|
|
||||||
|
The guide doesn’t assume you know anything about fine-grained reactivity or the
|
||||||
|
details of modern Web frameworks. It does assume you are familiar with the Rust
|
||||||
|
programming language, HTML, CSS, and the DOM and basic Web APIs.
|
||||||
|
|
||||||
|
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
|
||||||
|
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
|
||||||
|
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
|
||||||
|
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
|
||||||
|
understand Leptos.
|
||||||
|
|
||||||
|
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
|
||||||
|
|
||||||
|
**The guide is a work in progress.**
|
48
docs/book/src/02_getting_started.md
Normal file
48
docs/book/src/02_getting_started.md
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
There are two basic paths to getting started with Leptos:
|
||||||
|
1. Client-side rendering with [Trunk](https://trunkrs.dev/)
|
||||||
|
2. Full-stack rendering with [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos)
|
||||||
|
|
||||||
|
For the early examples, it will be easiest to begin with Trunk. We’ll introduce
|
||||||
|
`cargo-leptos` a little later in this series.
|
||||||
|
|
||||||
|
|
||||||
|
If you don’t already have it installed, you can install Trunk by running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install trunk
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a basic Rust binary project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo init leptos-tutorial
|
||||||
|
```
|
||||||
|
|
||||||
|
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||||
|
```bash
|
||||||
|
cargo add leptos
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
And add a simple “Hello, world!” to your `main.rs`
|
||||||
|
```rust
|
||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now run `trunk serve --open`. Trunk should automatically compile your app and
|
||||||
|
open it in your default browser. If you make edits to `main.rs`, Trunk will
|
||||||
|
recompile your source code and live-reload the page.
|
41
docs/book/src/SUMMARY.md
Normal file
41
docs/book/src/SUMMARY.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
- [Introduction](./01_introduction.md)
|
||||||
|
- [Getting Started](./02_getting_started.md)
|
||||||
|
- [Building User Interfaces](./view/README.md)
|
||||||
|
- [A Basic Component](./view/01_basic_component.md)
|
||||||
|
- [Dynamic Attributes](./view/02_dynamic_attributes.md)
|
||||||
|
- [Components and Props](./view/03_components.md)
|
||||||
|
- [Iteration](./view/04_iteration.md)
|
||||||
|
- [Forms and Inputs](./view/05_forms.md)
|
||||||
|
- [Control Flow](./view/06_control_flow.md)
|
||||||
|
- [Error Handling](./view/07_errors.md)
|
||||||
|
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||||
|
- [Passing Children to Components]()
|
||||||
|
- [Interlude: Reactivity and Functions]()
|
||||||
|
- [Testing]()
|
||||||
|
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||||
|
- [Async]()
|
||||||
|
- [Resource]()
|
||||||
|
- [Suspense]()
|
||||||
|
- [Transition]()
|
||||||
|
- [State Management]()
|
||||||
|
- [Interlude: Advanced Reactivity]()
|
||||||
|
- [Router]()
|
||||||
|
- [Fundamentals]()
|
||||||
|
- [defining `<Routes/>`]()
|
||||||
|
- [`<A/>`]()
|
||||||
|
- [`<Form/>`]()
|
||||||
|
- [Metadata]()
|
||||||
|
- [SSR]()
|
||||||
|
- [Models of SSR]()
|
||||||
|
- [`cargo-leptos`]()
|
||||||
|
- [Hydration Footguns]()
|
||||||
|
- [Request/Response]()
|
||||||
|
- [Headers]()
|
||||||
|
- [Cookies]()
|
||||||
|
- [Server Functions]()
|
||||||
|
- [Actions]()
|
||||||
|
- [Forms]()
|
||||||
|
- [`<ActionForm/>`s]()
|
||||||
|
- [Turning off WebAssembly]()
|
143
docs/book/src/view/01_basic_component.md
Normal file
143
docs/book/src/view/01_basic_component.md
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
# A Basic Component
|
||||||
|
|
||||||
|
That “Hello, world!” was a *very* simple example. Let’s move on to something a
|
||||||
|
little more like an ordinary app.
|
||||||
|
|
||||||
|
First, let’s edit the `main` function so that, instead of rendering the whole
|
||||||
|
app, it just renders an `<App/>` component. Components are the basic unit of
|
||||||
|
composition and design in most web frameworks, and Leptos is no exception.
|
||||||
|
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||||
|
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||||
|
`PascalCase`, so most Leptos applications will start with something like an
|
||||||
|
`<App/>` component.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let’s define our `<App/>` component itself. Because it’s relatively simple,
|
||||||
|
I’ll give you the whole thing up front, then walk through it line by line.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (count, set_count) = create_signal(cx, 0);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<button
|
||||||
|
on:click=move |_| {
|
||||||
|
set_count.update(|n| *n += 1);
|
||||||
|
}
|
||||||
|
>
|
||||||
|
"Click me"
|
||||||
|
{move || count.get()}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Component Signature
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
```
|
||||||
|
|
||||||
|
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||||
|
used as a component in your Leptos application. We’ll see some of the other features of
|
||||||
|
this macro in a couple chapters.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn App(cx: Scope) -> impl IntoView
|
||||||
|
```
|
||||||
|
|
||||||
|
Every component is a function with the following characteristics
|
||||||
|
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||||
|
as its first argument. This `Scope` is our entrypoint into the reactive system.
|
||||||
|
By convention, it’s usually named `cx`.
|
||||||
|
2. You can include other arguments, which will be available as component “props.”
|
||||||
|
3. Component functions return `impl IntoView`, which is an opaque type that includes
|
||||||
|
anything you could return from a Leptos `view`.
|
||||||
|
|
||||||
|
## The Component Body
|
||||||
|
The body of the component function is a set-up function that runs once, not a
|
||||||
|
render function that re-runs multiple times. You’ll typically use it to create a
|
||||||
|
few reactive variables, define any side effects that run in response to those values
|
||||||
|
changing, and describe the user interface.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (count, set_count) = create_signal(cx, 0);
|
||||||
|
```
|
||||||
|
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
|
||||||
|
creates a signal, the basic unit of reactive change and state management in Leptos.
|
||||||
|
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||||
|
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||||
|
current value, you’ll call `set_count.set(...)` (or `set_count(...)`).
|
||||||
|
|
||||||
|
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||||
|
efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||||
|
|
||||||
|
## The View
|
||||||
|
|
||||||
|
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
view! { cx,
|
||||||
|
<button
|
||||||
|
// define an event listener with on:
|
||||||
|
on:click=move |_| {
|
||||||
|
set_count.update(|n| *n += 1);
|
||||||
|
}
|
||||||
|
>
|
||||||
|
// text nodes are wrapped in quotation marks
|
||||||
|
"Click me: "
|
||||||
|
// blocks can include Rust code
|
||||||
|
{move || count.get()}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This should mostly be easy to understand: it looks like HTML, with a special
|
||||||
|
`on:click` to define a `click` event listener, a text node that’s formatted like
|
||||||
|
a Rust string, and then...
|
||||||
|
```rust
|
||||||
|
{move || count.get()}
|
||||||
|
```
|
||||||
|
whatever that is.
|
||||||
|
|
||||||
|
People sometimes joke that they use more closures in their first Leptos application
|
||||||
|
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||||
|
into the view tells the framework: “Hey, this is something that might change.”
|
||||||
|
|
||||||
|
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||||
|
`move || count.get()` closure, whose value depends on the value of `count`, re-runs,
|
||||||
|
and the framework makes a targeted update to that one specific text node, touching
|
||||||
|
nothing else in your application. This is what allows for extremely efficient updates
|
||||||
|
to the DOM.
|
||||||
|
|
||||||
|
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
|
||||||
|
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||||
|
Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary.
|
||||||
|
As a result, you can write a simpler view:
|
||||||
|
```rust
|
||||||
|
view! { cx,
|
||||||
|
<button /* ... */>
|
||||||
|
"Click me: "
|
||||||
|
// identical to {move || count.get()}
|
||||||
|
{count}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember—and this is *very important*—only functions are reactive. This means that
|
||||||
|
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||||
|
in a function, telling the framework to update the view every time `count` changes.
|
||||||
|
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||||
|
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||||
|
|
||||||
|
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||||
|
show the browser in the sandbox, you may need to click `Add DevTools >
|
||||||
|
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||||
|
and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
104
docs/book/src/view/02_dynamic_attributes.md
Normal file
104
docs/book/src/view/02_dynamic_attributes.md
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# `view`: Dynamic Attributes and Classes
|
||||||
|
|
||||||
|
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||||
|
create dynamic text by passing a function (such as a signal) into the view.
|
||||||
|
|
||||||
|
But of course there are other things you might want to update in your user interface.
|
||||||
|
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||||
|
and we’ll introduce the concept of a **derived signal**.
|
||||||
|
|
||||||
|
Let’s start with a simple component that should be familiar: click a button to
|
||||||
|
increment a counter.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (count, set_count) = create_signal(cx, 0);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<button
|
||||||
|
on:click=move |_| {
|
||||||
|
set_count.update(|n| *n += 1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So far, this is just the example from the last chapter.
|
||||||
|
|
||||||
|
## Dynamic Classes
|
||||||
|
|
||||||
|
Now let’s say I’d like to update the list of CSS classes on this element dynamically.
|
||||||
|
For example, let’s say I want to add the class `red` when the count is odd. I can
|
||||||
|
do this using the `class:` syntax.
|
||||||
|
```rust
|
||||||
|
class:red=move || count() & 1 == 1
|
||||||
|
```
|
||||||
|
`class:` attributes take
|
||||||
|
1. the class name, following the colon (`red`)
|
||||||
|
2. a value, which can be a `bool` or a function that returns a `bool`
|
||||||
|
|
||||||
|
When the value is `true`, the class is added. When the value is `false`, the class
|
||||||
|
is removed. And if the value is a function that accesses a signal, the class will
|
||||||
|
reactively update when the signal changes.
|
||||||
|
|
||||||
|
Now every time I click the button, the text should toggle between red and black as
|
||||||
|
the number switches between even and odd.
|
||||||
|
|
||||||
|
## Dynamic Attributes
|
||||||
|
|
||||||
|
The same applies to plain attributes. Passing a plain string or primitive value to
|
||||||
|
an attribute gives it a static value. Passing a function (including a signal) to
|
||||||
|
an attribute causes it to update its value reactively. Let’s add another element
|
||||||
|
to our view:
|
||||||
|
```rust
|
||||||
|
<progress
|
||||||
|
max="50"
|
||||||
|
// signals are functions, so this <=> `move || count.get()`
|
||||||
|
value=count
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||||
|
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||||
|
our progress bar will move forward.
|
||||||
|
|
||||||
|
## Derived Signals
|
||||||
|
|
||||||
|
Let’s go one layer deeper, just for fun.
|
||||||
|
|
||||||
|
You already know that we create reactive interfaces just by passing functions into
|
||||||
|
the `view`. This means that we can easily change our progress bar. For example,
|
||||||
|
suppose we want it to move twice as fast:
|
||||||
|
```rust
|
||||||
|
<progress
|
||||||
|
max="50"
|
||||||
|
value=move || count() * 2
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
But imagine we want to reuse that calculation in more than one place. You can do this
|
||||||
|
using a **derived signal**: a closure that accesses a signal.
|
||||||
|
```rust
|
||||||
|
let double_count = move || count() * 2;
|
||||||
|
|
||||||
|
/* insert the rest of the view */
|
||||||
|
<progress
|
||||||
|
max="50"
|
||||||
|
// we use it once here
|
||||||
|
value=double_count
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
"Double Count: "
|
||||||
|
// and again here
|
||||||
|
{double_count}
|
||||||
|
</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Derived signals let you create reactive computed values that can be used in multiple
|
||||||
|
places in your application with minimal overhead.
|
||||||
|
|
||||||
|
> Note: Using a derived signal like this means that the calculation runs once per
|
||||||
|
signal change per place we access `double_count`; in other words, twice. This is a
|
||||||
|
very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||||
|
are designed to solve this problem for expensive calculations.
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
317
docs/book/src/view/03_components.md
Normal file
317
docs/book/src/view/03_components.md
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
# Components and Props
|
||||||
|
|
||||||
|
So far, we’ve been building our whole application in a single component. This
|
||||||
|
is fine for really tiny examples, but in any real application you’ll need to
|
||||||
|
break the user interface out into multiple components, so you can break your
|
||||||
|
interface down into smaller, reusable, composable chunks.
|
||||||
|
|
||||||
|
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||||
|
instead of one: one that advances one tick per click, one that advances two ticks
|
||||||
|
per click.
|
||||||
|
|
||||||
|
You _could_ do this by just creating two `<progress>` elements:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (count, set_count) = create_signal(cx, 0);
|
||||||
|
let double_count = move || count() * 2;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<progress
|
||||||
|
max="50"
|
||||||
|
value=progress
|
||||||
|
/>
|
||||||
|
<progress
|
||||||
|
max="50"
|
||||||
|
value=double_count
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
But of course, this doesn’t scale very well. If you want to add a third progress
|
||||||
|
bar, you need to add this code another time. And if you want to edit anything
|
||||||
|
about it, you need to edit it in triplicate.
|
||||||
|
|
||||||
|
Instead, let’s create a `<ProgressBar/>` component.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn ProgressBar(
|
||||||
|
cx: Scope
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<progress
|
||||||
|
max="50"
|
||||||
|
// hmm... where will we get this from?
|
||||||
|
value=progress
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||||
|
When we were defining everything manually, we just used the local variable names.
|
||||||
|
Now we need some way to pass an argument into the component.
|
||||||
|
|
||||||
|
## Component Props
|
||||||
|
|
||||||
|
We do this using component properties, or “props.” If you’ve used another frontend
|
||||||
|
framework, this is probably a familiar idea. Basically, properties are to components
|
||||||
|
as attributes are to HTML elements: they let you pass additional information into
|
||||||
|
the component.
|
||||||
|
|
||||||
|
In Leptos, you define props by giving additional arguments to the component function.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn ProgressBar(
|
||||||
|
cx: Scope,
|
||||||
|
progress: ReadSignal<i32>
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<progress
|
||||||
|
max="50"
|
||||||
|
// now this works
|
||||||
|
value=progress
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can use our component in the main `<App/>` component’s view.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (count, set_count) = create_signal(cx, 0);
|
||||||
|
view! { cx,
|
||||||
|
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||||
|
"Click me"
|
||||||
|
</button>
|
||||||
|
// now we use our component!
|
||||||
|
<ProgressBar progress=count/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||||
|
notice that you can easily tell the difference between an element and a component
|
||||||
|
because components always have `PascalCase` names. You pass the `progress` prop
|
||||||
|
in as if it were an HTML element attribute. Simple.
|
||||||
|
|
||||||
|
> ### Important Note
|
||||||
|
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||||
|
is what allows us to have named props, when Rust does not have named function parameters.
|
||||||
|
If you’re defining a component in one module and importing it into another, make
|
||||||
|
sure you include this `ComponentProps` type:
|
||||||
|
>
|
||||||
|
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||||
|
|
||||||
|
### Reactive and Static Props
|
||||||
|
|
||||||
|
You’ll notice that throughout this example, `progress` takes a reactive
|
||||||
|
`ReadSignal<i32>`, and not a plain `i32`. This is **very important**.
|
||||||
|
|
||||||
|
Component props have no special meaning attached to them. A component is simply
|
||||||
|
a function that runs once to set up the user interface. The only way to tell the
|
||||||
|
interface to respond to changing is to pass it a signal type. So if you have a
|
||||||
|
component property that will change over time, like our `progress`, it should
|
||||||
|
be a signal.
|
||||||
|
|
||||||
|
### `optional` Props
|
||||||
|
|
||||||
|
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||||
|
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||||
|
argument to the component function with `#[prop(optional)]`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn ProgressBar(
|
||||||
|
cx: Scope,
|
||||||
|
// mark this prop optional
|
||||||
|
// you can specify it or not when you use <ProgressBar/>
|
||||||
|
#[prop(optional)]
|
||||||
|
max: u16,
|
||||||
|
progress: ReadSignal<i32>
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<progress
|
||||||
|
max=max
|
||||||
|
value=progress
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max`
|
||||||
|
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||||
|
on an `optional` is its `Default::default()` value, which for a `u16` is going to
|
||||||
|
be `0`. In the case of a progress bar, a max value of `0` is not very useful.
|
||||||
|
|
||||||
|
So let’s give it a particular default value instead.
|
||||||
|
|
||||||
|
### `default` props
|
||||||
|
|
||||||
|
You can specify a default value other than `Default::default()` pretty simply
|
||||||
|
with `#[prop(default = ...)`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn ProgressBar(
|
||||||
|
cx: Scope,
|
||||||
|
#[prop(default = 100)]
|
||||||
|
max: u16,
|
||||||
|
progress: ReadSignal<i32>
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<progress
|
||||||
|
max=max
|
||||||
|
value=progress
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic Props
|
||||||
|
|
||||||
|
This is great. But we began with two counters, one driven by `count`, and one by
|
||||||
|
the derived signal `double_count`. Let’s recreate that by using `double_count`
|
||||||
|
as the `progress` prop on another `<ProgressBar/>`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (count, set_count) = create_signal(cx, 0);
|
||||||
|
let double_count = move || count() * 2;
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||||
|
"Click me"
|
||||||
|
</button>
|
||||||
|
<ProgressBar progress=count/>
|
||||||
|
// add a second progress bar
|
||||||
|
<ProgressBar progress=double_count/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Hm... this won’t compile. It should be pretty easy to understand why: we’ve declared
|
||||||
|
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||||
|
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||||
|
it’s a closure that returns an `i32`.
|
||||||
|
|
||||||
|
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||||
|
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||||
|
could just take any function?” If you’re savvy, you may know that both these
|
||||||
|
implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn ProgressBar<F>(
|
||||||
|
cx: Scope,
|
||||||
|
#[prop(default = 100)]
|
||||||
|
max: u16,
|
||||||
|
progress: F
|
||||||
|
) -> impl IntoView
|
||||||
|
where
|
||||||
|
F: Fn() -> i32
|
||||||
|
{
|
||||||
|
view! { cx,
|
||||||
|
<progress
|
||||||
|
max=max
|
||||||
|
value=progress
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||||
|
any value that implements this `Fn()` trait.
|
||||||
|
|
||||||
|
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||||
|
or as `progress: impl Fn() -> i32`, in part because they’re actually used to generate
|
||||||
|
a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||||
|
|
||||||
|
### `into` Props
|
||||||
|
|
||||||
|
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||||
|
This attribute automatically calls `.into()` on the values you pass as proprs,
|
||||||
|
which allows you to pass props of different values easily.
|
||||||
|
|
||||||
|
In this case, it’s helpful to know about the
|
||||||
|
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||||
|
is a enumerated type that represents any kind of readable reactive signal. It can
|
||||||
|
be useful when defining APIs for components you’ll want to reuse while passing
|
||||||
|
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||||
|
reactive value.
|
||||||
|
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn ProgressBar(
|
||||||
|
cx: Scope,
|
||||||
|
#[prop(default = 100)]
|
||||||
|
max: u16,
|
||||||
|
#[prop(into)]
|
||||||
|
progress: Signal<i32>
|
||||||
|
) -> impl IntoView
|
||||||
|
{
|
||||||
|
view! { cx,
|
||||||
|
<progress
|
||||||
|
max=max
|
||||||
|
value=progress
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (count, set_count) = create_signal(cx, 0);
|
||||||
|
let double_count = move || count() * 2;
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||||
|
"Click me"
|
||||||
|
</button>
|
||||||
|
// .into() converts `ReadSignal` to `Signal`
|
||||||
|
<ProgressBar progress=count/>
|
||||||
|
// use `Signal::derive()` to wrap a derived signal
|
||||||
|
<ProgressBar progress=Signal::derive(cx, double_count)/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documenting Components
|
||||||
|
|
||||||
|
This is one of the least essential but most important sections of this book.
|
||||||
|
It’s not strictly necessary to document your components and their props. It may
|
||||||
|
be very important, depending on the size of your team and your app. But it’s very
|
||||||
|
easy, and bears immediate fruit.
|
||||||
|
|
||||||
|
To document a component and its props, you can simply add doc comments on the
|
||||||
|
component function, and each one of the props:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Shows progress toward a goal.
|
||||||
|
#[component]
|
||||||
|
fn ProgressBar(
|
||||||
|
cx: Scope,
|
||||||
|
/// The maximum value of the progress bar.
|
||||||
|
#[prop(default = 100)]
|
||||||
|
max: u16,
|
||||||
|
/// How much progress should be displayed.
|
||||||
|
#[prop(into)]
|
||||||
|
progress: Signal<i32>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That’s all you need to do. These behave like ordinary Rust doc comments, except
|
||||||
|
that you can document individual component props, which can’t be done with Rust
|
||||||
|
function arguments.
|
||||||
|
|
||||||
|
This will automatically generate documentation for your component, its `Props`
|
||||||
|
type, and each of the fields used to add props. It can be a little hard to
|
||||||
|
understand how powerful this is until you hover over the component name or props
|
||||||
|
and see the power of the `#[component]` macro combined with rust-analyzer here.
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px"></iframe>
|
88
docs/book/src/view/04_iteration.md
Normal file
88
docs/book/src/view/04_iteration.md
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# Iteration
|
||||||
|
|
||||||
|
Whether you’re listing todos, displaying a table, or showing product images,
|
||||||
|
iterating over a list of items is a common task in web applications. Reconciling
|
||||||
|
the differences between changing sets of items can also be one of the trickiest
|
||||||
|
tasks for a framework to handle well.
|
||||||
|
|
||||||
|
Leptos supports to two different patterns for iterating over items:
|
||||||
|
1. For static views: `Vec<_>`
|
||||||
|
2. For dynamic lists: `<For/>`
|
||||||
|
|
||||||
|
## Static Views with `Vec<_>`
|
||||||
|
|
||||||
|
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||||
|
does not often change. In this case, it’s important to know that you can insert
|
||||||
|
any `Vec<IV> where IV: IntoView` into your view. In other views, if you can render
|
||||||
|
`T`, you can render `Vec<T>`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let values = vec![0, 1, 2];
|
||||||
|
view! { cx,
|
||||||
|
// this will just render "012"
|
||||||
|
<p>{values.clone()}</p>
|
||||||
|
// or we can wrap them in <li>
|
||||||
|
<ul>
|
||||||
|
{values.into_iter()
|
||||||
|
.map(|n| view! { cx, <li>{n}</li>})
|
||||||
|
.collect::<Vec<_>>()}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The fact that the _list_ is static doesn’t mean the interface needs to be static.
|
||||||
|
You can render dynamic items as part of a static list.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// create a list of N signals
|
||||||
|
let counters = (1..=length).map(|idx| create_signal(cx, idx));
|
||||||
|
|
||||||
|
// each item manages a reactive view
|
||||||
|
// but the list itself will never change
|
||||||
|
let counter_buttons = counters
|
||||||
|
.map(|(count, set_count)| {
|
||||||
|
view! { cx,
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
on:click=move |_| set_count.update(|n| *n += 1)
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<ul>{counter_buttons}</ul>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||||
|
it changes, this will rerender every item in the list. This is quite inefficient!
|
||||||
|
Fortunately, there’s a better way.
|
||||||
|
|
||||||
|
## Dynamic Rendering with the `<For/>` Component
|
||||||
|
|
||||||
|
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||||
|
keyed dynamic list. It takes three props:
|
||||||
|
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
|
||||||
|
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
|
||||||
|
- `view`: renders each `T` into a view
|
||||||
|
|
||||||
|
`key` is, well, the key. You can add, remove, and move items within the list. As
|
||||||
|
long as each item’s key is stable over time, the framework does not need to rerender
|
||||||
|
any of the items, unless they are new additions, and it can very efficiently add,
|
||||||
|
remove, and move items as they change. This allows for extremely efficient updates
|
||||||
|
to the list as it changes, with minimal additional work.
|
||||||
|
|
||||||
|
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||||
|
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||||
|
indices change.
|
||||||
|
|
||||||
|
But it’s a great idea to do something like generating a unique ID for each row as
|
||||||
|
it is generated, and using that as an ID for the key function.
|
||||||
|
|
||||||
|
Check out the `<DynamicList/>` component below for an example.
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="100px"></iframe>
|
107
docs/book/src/view/05_forms.md
Normal file
107
docs/book/src/view/05_forms.md
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
# Forms and Inputs
|
||||||
|
|
||||||
|
Forms and form inputs are an important part of interactive apps. There are two
|
||||||
|
basic patterns for interacting with inputs in Leptos, which you may recognize
|
||||||
|
if you’re familiar with React, SolidJS, or a similar framework: using **controlled**
|
||||||
|
or **uncontrolled** inputs.
|
||||||
|
|
||||||
|
## Controlled Inputs
|
||||||
|
|
||||||
|
In a "controlled input," the framework controls the state of the input
|
||||||
|
element. On every `input` event, it updates a local signal that holds the current
|
||||||
|
state, which in turn updates the `value` prop of the input.
|
||||||
|
|
||||||
|
There are two important things to remember:
|
||||||
|
1. The `input` event fires on (almost) every change to the element, while the
|
||||||
|
`change` event fires (more or less) when you unfocus the input. You probably
|
||||||
|
want `on:input`, but we give you the freedom to choose.
|
||||||
|
2. The `value` *attribute* only sets the initial value of the input, i.e., it
|
||||||
|
only updates the input up to the point that you begin typing. The `value`
|
||||||
|
*property* continues updating the input after that. You usually want to set
|
||||||
|
`prop:value` for this reason.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (name, set_name) = create_signal(cx, "Controlled".to_string());
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<input type="text"
|
||||||
|
on:input=move |ev| {
|
||||||
|
// event_target_value is a Leptos helper function
|
||||||
|
// it functions the same way as event.target.value
|
||||||
|
// in JavaScript, but smooths out some of the typecasting
|
||||||
|
// necessary to make this work in Rust
|
||||||
|
set_name(event_target_value(&ev));
|
||||||
|
}
|
||||||
|
|
||||||
|
// the `prop:` syntax lets you update a DOM property,
|
||||||
|
// rather than an attribute.
|
||||||
|
prop:value=name
|
||||||
|
/>
|
||||||
|
<p>"Name is: " {name}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uncontrolled Inputs
|
||||||
|
|
||||||
|
In an "uncontrolled input," the browser controls the state of the input element.
|
||||||
|
Rather than continuously updating a signal to hold its value, we use a
|
||||||
|
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||||
|
the input once when we want to get its value.
|
||||||
|
|
||||||
|
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||||
|
event.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||||
|
|
||||||
|
let input_element: NodeRef<HtmlElement<Input>> = NodeRef::new(cx);
|
||||||
|
```
|
||||||
|
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||||
|
underlying DOM node. Its value will be set when the element is rendered.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let on_submit = move |ev: SubmitEvent| {
|
||||||
|
// stop the page from reloading!
|
||||||
|
ev.prevent_default();
|
||||||
|
|
||||||
|
// here, we'll extract the value from the input
|
||||||
|
let value = input_element()
|
||||||
|
// event handlers can only fire after the view
|
||||||
|
// is mounted to the DOM, so the `NodeRef` will be `Some`
|
||||||
|
.expect("<input> to exist")
|
||||||
|
// `NodeRef` implements `Deref` for the DOM element type
|
||||||
|
// this means we can call`HtmlInputElement::value()`
|
||||||
|
// to get the current value of the input
|
||||||
|
.value();
|
||||||
|
set_name(value);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
Our `on_submit` handler will access the input’s value and use it to call `set_name`.
|
||||||
|
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
|
||||||
|
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||||
|
know it will already have been filled when we rendered the view, so it’s safe to
|
||||||
|
unwrap here.
|
||||||
|
|
||||||
|
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||||
|
gives us access to a correctly-typed HTML element.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
view! { cx,
|
||||||
|
<form on:submit=on_submit>
|
||||||
|
<input type="text"
|
||||||
|
value=name
|
||||||
|
node_ref=input_element
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Submit"/>
|
||||||
|
</form>
|
||||||
|
<p>"Name is: " {name}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The view should be pretty self-explanatory by now. Note two things:
|
||||||
|
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
|
||||||
|
This is because we’re just setting the initial value of the input, and letting
|
||||||
|
the browser control its state. (We could use `prop:value` instead.)
|
||||||
|
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
|
||||||
|
They are the same thing, but `node_ref` has better rust-analyzer support.)
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px"></iframe>
|
283
docs/book/src/view/06_control_flow.md
Normal file
283
docs/book/src/view/06_control_flow.md
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
# Control Flow
|
||||||
|
|
||||||
|
In most applications, you sometimes need to make a decision: Should I render this
|
||||||
|
part of the view, or not? Should I render `<ButtonA/>` or `<WidgetB/>`? This is
|
||||||
|
**control flow**.
|
||||||
|
|
||||||
|
## A Few Tips
|
||||||
|
|
||||||
|
When thinking about how to do this with Leptos, it’s important to remember a few
|
||||||
|
things:
|
||||||
|
|
||||||
|
1. Rust is an expression-oriented language: control-flow expressions like
|
||||||
|
`if x() { y } else { z }` and `match x() { ... }` return their values. This
|
||||||
|
makes them very useful for declarative user interfaces.
|
||||||
|
2. For any `T` that implements `IntoView`—in other words, for any type that Leptos
|
||||||
|
knows how to render—`Option<T>` and `Result<T, impl Error>` _also_ implement
|
||||||
|
`IntoView`. And just as `Fn() -> T` renders a reactive `T`, `Fn() -> Option<T>`
|
||||||
|
and `Fn() -> Result<T, impl Error>` are reactive.
|
||||||
|
3. Rust has lots of handy helpers like [Option::map](https://doc.rust-lang.org/std/option/enum.Option.html#method.map),
|
||||||
|
[Option::and_then](https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then),
|
||||||
|
[Option::ok_or](https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or),
|
||||||
|
[Result::map](https://doc.rust-lang.org/std/result/enum.Result.html#method.map),
|
||||||
|
[Result::ok](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok), and
|
||||||
|
[bool::then](https://doc.rust-lang.org/std/primitive.bool.html#method.then) that
|
||||||
|
allow you to convert, in a declarative way, between a few different standard types,
|
||||||
|
all of which can be rendered. Spending time in the `Option` and `Result` docs in particular
|
||||||
|
is one of the best ways to level up your Rust game.
|
||||||
|
4. And always remember: to be reactive, values must be functions. You’ll see me constantly
|
||||||
|
wrap things in a `move ||` closure, below. This is to ensure that they actually re-run
|
||||||
|
when the signal they depend on changes, keeping the UI reactive.
|
||||||
|
|
||||||
|
## So What?
|
||||||
|
|
||||||
|
To connect the dots a little: this means that you can actually implement most of
|
||||||
|
your control flow with native Rust code, without any control-flow components or
|
||||||
|
special knowledge.
|
||||||
|
|
||||||
|
For example, let’s start with a simple signal and derived signal:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (value, set_value) = create_signal(cx, 0);
|
||||||
|
let is_odd = move || value() & 1 == 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
> If you don’t recognize what’s going on with `is_odd`, don’t worry about it
|
||||||
|
> too much. It’s just a simple way to test whether an integer is odd by doing a
|
||||||
|
> bitwise `AND` with `1`.
|
||||||
|
|
||||||
|
We can use these signals and ordinary Rust to build most control flow.
|
||||||
|
|
||||||
|
### `if` statements
|
||||||
|
|
||||||
|
Let’s say I want to render some text if the number is odd, and some other text
|
||||||
|
if it’s even. Well, how about this?
|
||||||
|
|
||||||
|
```rust
|
||||||
|
view! { cx,
|
||||||
|
<p>
|
||||||
|
{move || if is_odd() {
|
||||||
|
"Odd"
|
||||||
|
} else {
|
||||||
|
"Even"
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An `if` expression returns its value, and a `&str` implements `IntoView`, so a
|
||||||
|
`Fn() -> &str` implements `IntoView`, so this... just works!
|
||||||
|
|
||||||
|
### `Option<T>`
|
||||||
|
|
||||||
|
Let’s say we want to render some text if it’s odd, and nothing if it’s even.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let message = move || {
|
||||||
|
if is_odd() {
|
||||||
|
Some("Ding ding ding!")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<p>{message}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This works fine. We can make it a little shorter if we’d like, using `bool::then()`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let message = move || is_odd().then(|| "Ding ding ding!");
|
||||||
|
view! { cx,
|
||||||
|
<p>{message}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You could even inline this if you’d like, although personally I sometimes like the
|
||||||
|
better `cargo fmt` and `rust-analyzer` support I get by pulling things out of the `view`.
|
||||||
|
|
||||||
|
### `match` statements
|
||||||
|
|
||||||
|
We’re still just writing ordinary Rust code, right? So you have all the power of Rust’s
|
||||||
|
pattern matching at your disposal.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let message = move || {
|
||||||
|
match value() {
|
||||||
|
0 => "Zero",
|
||||||
|
1 => "One",
|
||||||
|
n if is_odd() => "Odd",
|
||||||
|
_ => "Even"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view! { cx,
|
||||||
|
<p>{message}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And why not? YOLO, right?
|
||||||
|
|
||||||
|
## Preventing Over-Rendering
|
||||||
|
|
||||||
|
Not so YOLO.
|
||||||
|
|
||||||
|
Everything we’ve just done is basically fine. But there’s one thing you should remember
|
||||||
|
and try to be careful with. Each one of the control-flow functions we’ve created so far
|
||||||
|
is basically a derived signal: it will rerun every time the value changes. In the examples
|
||||||
|
above, where the value switches from even to odd on every change, this is fine.
|
||||||
|
|
||||||
|
But consider the following example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (value, set_value) = create_signal(cx, 0);
|
||||||
|
|
||||||
|
let message = move || if value() > 5 {
|
||||||
|
"Big"
|
||||||
|
} else {
|
||||||
|
"Small"
|
||||||
|
};
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<p>{message}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This _works_, for sure. But if you added a log, you might be surprised
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let message = move || if value() > 5 {
|
||||||
|
log!("{}: rendering Big", value());
|
||||||
|
"Big"
|
||||||
|
} else {
|
||||||
|
log!("{}: rendering Small", value());
|
||||||
|
"Small"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
As a user clicks a button, you’d see something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
1: rendering Small
|
||||||
|
2: rendering Small
|
||||||
|
3: rendering Small
|
||||||
|
4: rendering Small
|
||||||
|
5: rendering Small
|
||||||
|
6: rendering Big
|
||||||
|
7: rendering Big
|
||||||
|
8: rendering Big
|
||||||
|
... ad infinitum
|
||||||
|
```
|
||||||
|
|
||||||
|
Every time `value` changes, it reruns the `if` statement. This makes sense, with
|
||||||
|
how reactivity works. But it has a downside. For a simple text node, rerunning
|
||||||
|
the `if` statement and rerendering isn’t a big deal. But imagine it were
|
||||||
|
like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let message = move || if value() > 5 {
|
||||||
|
<Big/>
|
||||||
|
} else {
|
||||||
|
<Small/>
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This rerenders `<Small/>` five times, then `<Big/>` infinitely. If they’re
|
||||||
|
loading resources, creating signals, or even just creating DOM nodes, this is
|
||||||
|
unnecessary work.
|
||||||
|
|
||||||
|
The [`<Show/>`](https://docs.rs/leptos/latest/leptos/fn.Show.html) component is
|
||||||
|
the answer. You pass it a `when` condition function, a `fallback` to be shown if
|
||||||
|
the `when` function returns `false`, and children to be rendered if `when` is `true`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (value, set_value) = create_signal(cx, 0);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<Show
|
||||||
|
when=move || value() > 5
|
||||||
|
fallback=|cx| view! { cx, <Small/> }
|
||||||
|
>
|
||||||
|
<Big/>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`<Show/>` memoizes the `when` condition, so it only renders its `<Small/>` once,
|
||||||
|
continuing to show the same component until `value` is greater than five;
|
||||||
|
then it renders `<Big/>` once, continuing to show it indefinitely.
|
||||||
|
|
||||||
|
This is a helpful tool to avoid rerendering when using dynamic `if` expressions.
|
||||||
|
As always, there's some overhead: for a very simple node (like updating a single
|
||||||
|
text node, or updating a class or attribute), a `move || if ...` will be more
|
||||||
|
efficient. But if it’s at all expensive to render either branch, reach for
|
||||||
|
`<Show/>`.
|
||||||
|
|
||||||
|
## Note: Type Conversions
|
||||||
|
|
||||||
|
There‘s one final thing it’s important to say in this section.
|
||||||
|
|
||||||
|
The `view` macro doesn’t return the most-generic wrapping type
|
||||||
|
[`View`](https://docs.rs/leptos/latest/leptos/enum.View.html).
|
||||||
|
Instead, it returns things with types like `Fragment` or `HtmlElement<Input>`. This
|
||||||
|
can be a little annoying if you’re returning different HTML elements from
|
||||||
|
different branches of a conditional:
|
||||||
|
|
||||||
|
```rust,compile_error
|
||||||
|
view! { cx,
|
||||||
|
<main>
|
||||||
|
{move || match is_odd() {
|
||||||
|
true if value() == 1 => {
|
||||||
|
// returns HtmlElement<Pre>
|
||||||
|
view! { cx, <pre>"One"</pre> }
|
||||||
|
},
|
||||||
|
false if value() == 2 => {
|
||||||
|
// returns HtmlElement<P>
|
||||||
|
view! { cx, <p>"Two"</p> }
|
||||||
|
}
|
||||||
|
// returns HtmlElement<Textarea>
|
||||||
|
_ => view! { cx, <textarea>{value()}</textarea> }
|
||||||
|
}}
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This strong typing is actually very powerful, because
|
||||||
|
[`HtmlElement`](https://docs.rs/leptos/0.1.3/leptos/struct.HtmlElement.html) is,
|
||||||
|
among other things, a smart pointer: each `HtmlElement<T>` type implements
|
||||||
|
`Deref` for the appropriate underlying `web_sys` type. In other words, in the browser
|
||||||
|
your `view` returns real DOM elements, and you can access native DOM methods on
|
||||||
|
them.
|
||||||
|
|
||||||
|
But it can be a little annoying in conditional logic like this, because you can’t
|
||||||
|
return different types from different branches of a condition in Rust. There are two ways
|
||||||
|
to get yourself out of this situation:
|
||||||
|
|
||||||
|
1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>`
|
||||||
|
with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any)
|
||||||
|
2. If you have a variety of view types that are not all `HtmlElement`, convert them to
|
||||||
|
`View`s with [`.into_view(cx)`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
|
||||||
|
|
||||||
|
Here’s the same example, with the conversion added:
|
||||||
|
|
||||||
|
```rust,compile_error
|
||||||
|
view! { cx,
|
||||||
|
<main>
|
||||||
|
{move || match is_odd() {
|
||||||
|
true if value() == 1 => {
|
||||||
|
// returns HtmlElement<Pre>
|
||||||
|
view! { cx, <pre>"One"</pre> }.into_any()
|
||||||
|
},
|
||||||
|
false if value() == 2 => {
|
||||||
|
// returns HtmlElement<P>
|
||||||
|
view! { cx, <p>"Two"</p> }.into_any()
|
||||||
|
}
|
||||||
|
// returns HtmlElement<Textarea>
|
||||||
|
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||||
|
}}
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
115
docs/book/src/view/07_errors.md
Normal file
115
docs/book/src/view/07_errors.md
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# Error Handling
|
||||||
|
|
||||||
|
[In the last chapter](./06_control_flow.md), we saw that you can render `Option<T>`:
|
||||||
|
in the `None` case, it will render nothing, and in the `T` case, it will render `T`
|
||||||
|
(that is, if `T` implements `IntoView`). You can actually do something very similar
|
||||||
|
with a `Result<T, E>`. In the `Err(_)` case, it will render nothing. In the `Ok(T)`
|
||||||
|
case, it will render the `T`.
|
||||||
|
|
||||||
|
Let’s start with a simple component to capture a number input.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||||
|
let (value, set_value) = create_signal(cx, Ok(0));
|
||||||
|
|
||||||
|
// when input changes, try to parse a number from the input
|
||||||
|
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<label>
|
||||||
|
"Type a number (or not!)"
|
||||||
|
<input type="number" on:input=on_input/>
|
||||||
|
<p>
|
||||||
|
"You entered "
|
||||||
|
<strong>{value}</strong>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every time you change the input, `on_input` will attempt to parse its value into a 32-bit
|
||||||
|
integer (`i32`), and store it in our `value` signal, which is a `Result<i32, _>`. If you
|
||||||
|
type the number `42`, the UI will display
|
||||||
|
|
||||||
|
```
|
||||||
|
You entered 42
|
||||||
|
```
|
||||||
|
|
||||||
|
But if you type the string`foo`, it will display
|
||||||
|
|
||||||
|
```
|
||||||
|
You entered
|
||||||
|
```
|
||||||
|
|
||||||
|
This is not great. It saves us using `.unwrap_or_default()` or something, but it would be
|
||||||
|
much nicer if we could catch the error and do something with it.
|
||||||
|
|
||||||
|
You can do that, with the [`<ErrorBoundary/>`](https://docs.rs/leptos/latest/leptos/fn.ErrorBoundary.html)
|
||||||
|
component.
|
||||||
|
|
||||||
|
## `<ErrorBoundary/>`
|
||||||
|
|
||||||
|
An `<ErrorBoundary/>` is a little like the `<Show/>` component we saw in the last chapter.
|
||||||
|
If everything’s okay—which is to say, if everything is `Ok(_)`—it renders its children.
|
||||||
|
But if there’s an `Err(_)` rendered among those children, it will trigger the
|
||||||
|
`<ErrorBoundary/>`’s `fallback`.
|
||||||
|
|
||||||
|
Let’s add an `<ErrorBoundary/>` to this example.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||||
|
let (value, set_value) = create_signal(cx, Ok(0));
|
||||||
|
|
||||||
|
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<h1>"Error Handling"</h1>
|
||||||
|
<label>
|
||||||
|
"Type a number (or something that's not a number!)"
|
||||||
|
<input type="number" on:input=on_input/>
|
||||||
|
<ErrorBoundary
|
||||||
|
// the fallback receives a signal containing current errors
|
||||||
|
fallback=|cx, errors| view! { cx,
|
||||||
|
<div class="error">
|
||||||
|
<p>"Not a number! Errors: "</p>
|
||||||
|
// we can render a list of errors as strings, if we'd like
|
||||||
|
<ul>
|
||||||
|
{move || errors.unwrap()
|
||||||
|
.get()
|
||||||
|
.0
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>"You entered " <strong>{value}</strong></p>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, if you type `42`, `value` is `Ok(42)` and you’ll see
|
||||||
|
|
||||||
|
```
|
||||||
|
You entered 42
|
||||||
|
```
|
||||||
|
|
||||||
|
If you type `foo`, value is `Err(_)` and the `fallback` will render. We’ve chosen to render
|
||||||
|
the list of errors as a `String`, so you’ll see something like
|
||||||
|
|
||||||
|
```
|
||||||
|
Not a number! Errors:
|
||||||
|
- cannot parse integer from empty string
|
||||||
|
```
|
||||||
|
|
||||||
|
If you fix the error, the error message will disappear and the content you’re wrapping in
|
||||||
|
an `<ErrorBoundary/>` will appear again.
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
286
docs/book/src/view/08_parent_child.md
Normal file
286
docs/book/src/view/08_parent_child.md
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
# Parent-Child Communication
|
||||||
|
|
||||||
|
You can think of your application as a nested tree of components. Each component
|
||||||
|
handles its own local state and manages a section of the user interface, so
|
||||||
|
components tend to be relatively self-contained.
|
||||||
|
|
||||||
|
Sometimes, though, you’ll want to communicate between a parent component and its
|
||||||
|
child. For example, imagine you’ve defined a `<FancyButton/>` component that adds
|
||||||
|
some styling, logging, or something else to a `<button/>`. You want to use a
|
||||||
|
`<FancyButton/>` in your `<App/>` component. But how can you communicate between
|
||||||
|
the two?
|
||||||
|
|
||||||
|
It’s easy to communicate state from a parent component to a child component. We
|
||||||
|
covered some of this in the material on [components and props](./03_components.md).
|
||||||
|
Basically if you want the parent to communicate to the child, you can pass a
|
||||||
|
[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a
|
||||||
|
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a
|
||||||
|
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/struct.MaybeSignal.html) as a prop.
|
||||||
|
|
||||||
|
But what about the other direction? How can a child send notifications about events
|
||||||
|
or state changes back up to the parent?
|
||||||
|
|
||||||
|
There are four basic patterns of parent-child communication in Leptos.
|
||||||
|
|
||||||
|
## 1. Pass a [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html)
|
||||||
|
|
||||||
|
One approach is simply to pass a `WriteSignal` from the parent down to the child, and update
|
||||||
|
it in the child. This lets you manipulate the state of the parent from the child.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
pub fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (toggled, set_toggled) = create_signal(cx, false);
|
||||||
|
view! { cx,
|
||||||
|
<p>"Toggled? " {toggled}</p>
|
||||||
|
<ButtonA setter=set_toggled/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<button
|
||||||
|
on:click=move |_| setter.update(|value| *value = !*value)
|
||||||
|
>
|
||||||
|
"Toggle"
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This pattern is simple, but you should be careful with it: passing around a `WriteSignal`
|
||||||
|
can make it hard to reason about your code. In this example, it’s pretty clear when you
|
||||||
|
read `<App/>` that you are handing off the ability to mutate `toggled`, but it’s not at
|
||||||
|
all clear when or how it will change. In this small, local example it’s easy to understand,
|
||||||
|
but if you find yourself passing around `WriteSignal`s like this throughout your code,
|
||||||
|
you should really consider whether this is making it too easy to write spaghetti code.
|
||||||
|
|
||||||
|
## 2. Use a Callback
|
||||||
|
|
||||||
|
Another approach would be to pass a callback to the child: say, `on_click`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
pub fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (toggled, set_toggled) = create_signal(cx, false);
|
||||||
|
view! { cx,
|
||||||
|
<p>"Toggled? " {toggled}</p>
|
||||||
|
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ButtonB<F>(
|
||||||
|
cx: Scope,
|
||||||
|
on_click: F,
|
||||||
|
) -> impl IntoView
|
||||||
|
where
|
||||||
|
F: Fn(MouseEvent) + 'static,
|
||||||
|
{
|
||||||
|
view! { cx,
|
||||||
|
<button on:click=on_click>
|
||||||
|
"Toggle"
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You’ll notice that whereas `<ButtonA/>` was given a `WriteSignal` and decided how to mutate it,
|
||||||
|
`<ButtonB/>` simply fires an event: the mutation happens back in `<App/>`. This has the advantage
|
||||||
|
of keeping local state local, preventing the problem of spaghetti mutation. But it also means
|
||||||
|
the logic to mutate that signal needs to exist up in `<App/>`, not down in `<ButtonB/>`. These
|
||||||
|
are real trade-offs, not a simple right-or-wrong choice.
|
||||||
|
|
||||||
|
> Note the way we declare the generic type `F` here for the callback. If you’re
|
||||||
|
> confused, look back at the [generic props](./03_components.html#generic-props) section
|
||||||
|
> of the chapter on components.
|
||||||
|
|
||||||
|
## 3. Use an Event Listener
|
||||||
|
|
||||||
|
You can actually write Option 2 in a slightly different way. If the callback maps directly onto
|
||||||
|
a native DOM event, you can add an `on:` listener directly to the place you use the component
|
||||||
|
in your `view` macro in `<App/>`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
pub fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (toggled, set_toggled) = create_signal(cx, false);
|
||||||
|
view! { cx,
|
||||||
|
<p>"Toggled? " {toggled}</p>
|
||||||
|
// note the on:click instead of on_click
|
||||||
|
// this is the same syntax as an HTML element event listener
|
||||||
|
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ButtonC<F>(cx: Scope) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<button>"Toggle"</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This lets you write way less code in `<ButtonC/>` than you did for `<ButtonB/>`,
|
||||||
|
and still gives a correctly-typed event to the listener. This works by adding an
|
||||||
|
`on:` event listener to each element that `<ButtonC/>` returns: in this case, just
|
||||||
|
the one `<button>`.
|
||||||
|
|
||||||
|
Of course, this only works for actual DOM events that you’re passing directly through
|
||||||
|
to the elements you’re rendering in the component. For more complex logic that
|
||||||
|
doesn’t map directly onto an element (say you create `<ValidatedForm/>` and want an
|
||||||
|
`on_valid_form_submit` callback) you should use Option 2.
|
||||||
|
|
||||||
|
## 4. Providing a Context
|
||||||
|
|
||||||
|
This version is actually a variant on Option 1. Say you have a deeply-nested component
|
||||||
|
tree:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
pub fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (toggled, set_toggled) = create_signal(cx, false);
|
||||||
|
view! { cx,
|
||||||
|
<p>"Toggled? " {toggled}</p>
|
||||||
|
<Layout/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Layout(cx: Scope) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<header>
|
||||||
|
<h1>"My Page"</h1>
|
||||||
|
<main>
|
||||||
|
<Content/>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Content(cx: Scope) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<div class="content">
|
||||||
|
<ButtonD/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ButtonD<F>(cx: Scope) -> impl IntoView {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now `<ButtonD/>` is no longer a direct child of `<App/>`, so you can’t simply
|
||||||
|
pass your `WriteSignal` to its props. You could do what’s sometimes called
|
||||||
|
“prop drilling,” adding a prop to each layer between the two:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
pub fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (toggled, set_toggled) = create_signal(cx, false);
|
||||||
|
view! { cx,
|
||||||
|
<p>"Toggled? " {toggled}</p>
|
||||||
|
<Layout set_toggled/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<header>
|
||||||
|
<h1>"My Page"</h1>
|
||||||
|
<main>
|
||||||
|
<Content set_toggled/>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||||
|
view! { cx,
|
||||||
|
<div class="content">
|
||||||
|
<ButtonD set_toggled/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ButtonD<F>(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a mess. `<Layout/>` and `<Content/>` don’t need `set_toggled`; they just
|
||||||
|
pass it through to `<ButtonD/>`. But I need to declare the prop in triplicate.
|
||||||
|
This is not only annoying but hard to maintain: imagine we add a “half-toggled”
|
||||||
|
option and the type of `set_toggled` needs to change to an `enum`. We have to change
|
||||||
|
it in three places!
|
||||||
|
|
||||||
|
Isn’t there some way to skip levels?
|
||||||
|
|
||||||
|
There is!
|
||||||
|
|
||||||
|
### The Context API
|
||||||
|
|
||||||
|
You can provide data that skips levels by using [`provide_context`](https://docs.rs/leptos/latest/leptos/fn.provide_context.html)
|
||||||
|
and [`use_context`](https://docs.rs/leptos/latest/leptos/fn.use_context.html). Contexts are identified
|
||||||
|
by the type of the data you provide (in this example, `WriteSignal<bool>`), and they exist in a top-down
|
||||||
|
tree that follows the contours of your UI tree. In this example, we can use context to skip the
|
||||||
|
unnecessary prop drilling.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
pub fn App(cx: Scope) -> impl IntoView {
|
||||||
|
let (toggled, set_toggled) = create_signal(cx, false);
|
||||||
|
|
||||||
|
// share `set_toggled` with all children of this component
|
||||||
|
provide_context(cx, set_toggled);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<p>"Toggled? " {toggled}</p>
|
||||||
|
<Layout/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// <Layout/> and <Content/> omitted
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||||
|
// use_context searches up the context tree, hoping to
|
||||||
|
// find a `WriteSignal<bool>`
|
||||||
|
// in this case, I .expect() because I know I provided it
|
||||||
|
let setter = use_context::<WriteSignal<bool>>(cx)
|
||||||
|
.expect("to have found the setter provided");
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<button
|
||||||
|
on:click=move |_| setter.update(|value| *value = !*value)
|
||||||
|
>
|
||||||
|
"Toggle"
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same caveats apply to this as to `<ButtonA/>`: passing a `WriteSignal`
|
||||||
|
around should be done with caution, as it allows you to mutate state from
|
||||||
|
arbitrary parts of your code. But when done carefully, this can be one of
|
||||||
|
the most effective techniques for global state management in Leptos: simply
|
||||||
|
provide the state at the highest level you’ll need it, and use it wherever
|
||||||
|
you need it lower down.
|
||||||
|
|
||||||
|
Note that there are no performance downsides to this approach. Because you
|
||||||
|
are passing a fine-grained reactive signal, _nothing happens_ in the intervening
|
||||||
|
components (`<Layout/>` and `<Content/>`) when you update it. You are communicating
|
||||||
|
directly between `<ButtonD/>` and `<App/>`. In fact—and this is the power of
|
||||||
|
fine-grained reactivity—you are communicating directly between a button click
|
||||||
|
in `<ButtonD/>` and a single text node in `<App/>`. It’s as if the components
|
||||||
|
themselves don’t exist at all. And, well... at runtime, they don’t. It’s just
|
||||||
|
signals and effects, all the way down.
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
5
docs/book/src/view/README.md
Normal file
5
docs/book/src/view/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Building User Interfaces
|
||||||
|
|
||||||
|
This first section will introduce you to the basic tools you need to build a reactive
|
||||||
|
user interface using Leptos. By the end of this section, you should be able to
|
||||||
|
build a simple, synchronous application that is rendered in the browser.
|
Loading…
Reference in a new issue