mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 19:32:37 -05:00
Compare commits
10 Commits
revert-538
...
async-docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e84c2a769 | ||
|
|
cc2b310e4c | ||
|
|
884348d7f8 | ||
|
|
dafd6e51c5 | ||
|
|
322041917d | ||
|
|
a2eaf9b3ee | ||
|
|
4032bfc210 | ||
|
|
4ff08f042b | ||
|
|
ce4b0ecbe1 | ||
|
|
6c31d09eb2 |
20
Cargo.toml
20
Cargo.toml
@@ -20,18 +20,18 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.0-alpha2"
|
||||
version = "0.2.0-beta"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_router = { path = "./router", version = "0.2.0-alpha2" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0-alpha2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.0-alpha2" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0-beta" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0-beta" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.2.0-beta" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.0-beta" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [Interlude: Reactivity and Functions](./interlude_functions.md)
|
||||
- [Testing](./testing.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Async]()
|
||||
- [Resource]()
|
||||
- [Suspense]()
|
||||
- [Async](./async/README.md)
|
||||
- [Loading Data with Resources](./async/10_resources.md)
|
||||
- [Suspense](./async/11_suspense.md)
|
||||
- [Transition]()
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [State Management]()
|
||||
- [Interlude: Advanced Reactivity]()
|
||||
- [Router]()
|
||||
|
||||
53
docs/book/src/async/10_resources.md
Normal file
53
docs/book/src/async/10_resources.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Loading Data with Resources
|
||||
|
||||
A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if it’s still pending.
|
||||
|
||||
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments (other than the ubiquitous `cx`):
|
||||
|
||||
1. a source signal, which will generate a new `Future` whenever it changes
|
||||
2. a fetcher function, which takes the data from that signal and returns a `Future`
|
||||
|
||||
Here’s an example
|
||||
|
||||
```rust
|
||||
// our source signal: some synchronous, local state
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// our resource
|
||||
let async_data = create_resource(cx,
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
To create a resource that simply runs once, you can pass a non-reactive, empty source signal:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
```
|
||||
|
||||
To access the value you can use `.read(cx)` or `.with(cx, |data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but with two differences
|
||||
|
||||
1. For any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
2. They take a `Scope` argument. You’ll see why in the next chapter, on `<Suspense/>`.
|
||||
|
||||
So, you can show the current state of a resource in your view:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
Resources also provide a `refetch()` method that allow you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?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>
|
||||
72
docs/book/src/async/11_suspense.md
Normal file
72
docs/book/src/async/11_suspense.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# `<Suspense/>`
|
||||
|
||||
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
But what if we have two resources, and want to wait for both of them?
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
That’s not _so_ bad, but it’s kind of annoying. What if we could invert the flow of control?
|
||||
|
||||
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If it’s still waiting for resources to load, it shows the `fallback`. When they’ve all loaded, it shows the children.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read(cx)
|
||||
.map(|a| view! { cx, <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read(cx)
|
||||
.map(|b| view! { cx, <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
```
|
||||
|
||||
Every time one of the resources is reloading, the `"Loading..."` fallback will show again.
|
||||
|
||||
This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?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>
|
||||
9
docs/book/src/async/12_transition.md
Normal file
9
docs/book/src/async/12_transition.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# `<Transition/>`
|
||||
|
||||
You’ll notice in the `<Suspense/>` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, there’s [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html).
|
||||
|
||||
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
|
||||
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. This can be a much better user experience than constantly falling back to a loading message.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
9
docs/book/src/async/README.md
Normal file
9
docs/book/src/async/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Working with `async`
|
||||
|
||||
So far we’ve only been working with synchronous users interfaces: You provide some input,
|
||||
the app immediately process it and updates the interface. This is great, but is a tiny
|
||||
subset of what web applications do. In particular, most web apps have to deal with some kind
|
||||
of asynchronous data loading, usually loading something from an API.
|
||||
|
||||
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
|
||||
In this chapter, we’ll see how Leptos helps smooth out that process for you.
|
||||
@@ -122,3 +122,5 @@ view! { cx,
|
||||
</WrappedChildren>
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
|
||||
@@ -97,10 +97,10 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
let value = move || counter.read().map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let error_msg = move || {
|
||||
counter
|
||||
.read()
|
||||
.read(cx)
|
||||
.map(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
@@ -143,7 +143,7 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
let value = move || {
|
||||
log::debug!("FormCounter looking for value");
|
||||
counter
|
||||
.read()
|
||||
.read(cx)
|
||||
.map(|n| n.ok())
|
||||
.flatten()
|
||||
.map(|n| n)
|
||||
|
||||
@@ -60,7 +60,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
// and by using the ErrorBoundary fallback to catch Err(_)
|
||||
// so we'll just implement our happy path and let the framework handle the rest
|
||||
let cats_view = move || {
|
||||
cats.with(|data| {
|
||||
cats.with(cx, |data| {
|
||||
data.iter()
|
||||
.flatten()
|
||||
.map(|cat| view! { cx, <img src={cat}/> })
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
@@ -82,7 +82,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
set_pending=set_pending.into()
|
||||
>
|
||||
{move || match stories.read() {
|
||||
{move || match stories.read(cx) {
|
||||
None => None,
|
||||
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
|
||||
Some(Some(stories)) => {
|
||||
|
||||
@@ -17,13 +17,13 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
|
||||
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
<Meta name="description" content=meta_description/>
|
||||
<Suspense fallback=|| view! { cx, "Loading..." }>
|
||||
{move || story.read().map(|story| match story {
|
||||
{move || story.read(cx).map(|story| match story {
|
||||
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! { cx,
|
||||
<div class="item-view">
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn User(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<div class="user-view">
|
||||
<Suspense fallback=|| view! { cx, "Loading..." }>
|
||||
{move || user.read().map(|user| match user {
|
||||
{move || user.read(cx).map(|user| match user {
|
||||
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
|
||||
Some(user) => view! { cx,
|
||||
<div>
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
@@ -82,7 +82,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
set_pending=set_pending.into()
|
||||
>
|
||||
{move || match stories.read() {
|
||||
{move || match stories.read(cx) {
|
||||
None => None,
|
||||
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
|
||||
Some(Some(stories)) => {
|
||||
|
||||
@@ -17,13 +17,13 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
|
||||
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
<Meta name="description" content=meta_description/>
|
||||
<Suspense fallback=|| view! { cx, "Loading..." }>
|
||||
{move || story.read().map(|story| match story {
|
||||
{move || story.read(cx).map(|story| match story {
|
||||
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! { cx,
|
||||
<div class="item-view">
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn User(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<div class="user-view">
|
||||
<Suspense fallback=|| view! { cx, "Loading..." }>
|
||||
{move || user.read().map(|user| match user {
|
||||
{move || user.read(cx).map(|user| match user {
|
||||
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
|
||||
Some(user) => view! { cx,
|
||||
<div>
|
||||
|
||||
@@ -7,7 +7,8 @@ use web_sys::MouseEvent;
|
||||
// for the child component to write into and the parent to read
|
||||
// 2) <ButtonB/>: passing a closure as one of the child component props, for
|
||||
// the child component to call
|
||||
// 4) <ButtonC/>: providing a context that is used in the component (rather than prop drilling)
|
||||
// 3) <ButtonC/>: adding an `on:` event listener to a component
|
||||
// 4) <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct SmallcapsContext(WriteSignal<bool>);
|
||||
@@ -17,6 +18,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// just some signals to toggle three classes on our <p>
|
||||
let (red, set_red) = create_signal(cx, false);
|
||||
let (right, set_right) = create_signal(cx, false);
|
||||
let (italics, set_italics) = create_signal(cx, false);
|
||||
let (smallcaps, set_smallcaps) = create_signal(cx, false);
|
||||
|
||||
// the newtype pattern isn't *necessary* here but is a good practice
|
||||
@@ -31,6 +33,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
|
||||
class:red=red
|
||||
class:right=right
|
||||
class:italics=italics
|
||||
class:smallcaps=smallcaps
|
||||
>
|
||||
"Lorem ipsum sit dolor amet."
|
||||
@@ -42,8 +45,13 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// Button B: pass a closure
|
||||
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
|
||||
|
||||
// Button B: use a regular event listener
|
||||
// setting an event listener on a component like this applies it
|
||||
// to each of the top-level elements the component returns
|
||||
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
|
||||
|
||||
// Button D gets its setter from context rather than props
|
||||
<ButtonC/>
|
||||
<ButtonD/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
@@ -53,7 +61,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonA(
|
||||
cx: Scope,
|
||||
/// Signal that will be toggled when the button is clicked.
|
||||
setter: WriteSignal<bool>
|
||||
setter: WriteSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
@@ -70,7 +78,7 @@ pub fn ButtonA(
|
||||
pub fn ButtonB<F>(
|
||||
cx: Scope,
|
||||
/// Callback that will be invoked when the button is clicked.
|
||||
on_click: F
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
@@ -97,10 +105,22 @@ where
|
||||
// if Rust ever had named function arguments we could drop this requirement
|
||||
}
|
||||
|
||||
/// Button C is a dummy: it renders a button but doesn't handle
|
||||
/// its click. Instead, the parent component adds an event listener.
|
||||
#[component]
|
||||
pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<button>
|
||||
"Toggle Italics"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
/// Button D is very similar to Button A, but instead of passing the setter as a prop
|
||||
/// we get it from the context
|
||||
#[component]
|
||||
pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||
let setter = use_context::<SmallcapsContext>(cx).unwrap().0;
|
||||
|
||||
view! {
|
||||
|
||||
@@ -71,9 +71,10 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
});
|
||||
|
||||
let location = use_location(cx);
|
||||
let contacts = create_resource(cx, move || location.search.get(), get_contacts);
|
||||
let contacts =
|
||||
create_resource(cx, move || location.search.get(), get_contacts);
|
||||
let contacts = move || {
|
||||
contacts.read().map(|contacts| {
|
||||
contacts.read(cx).map(|contacts| {
|
||||
// this data doesn't change frequently so we can use .map().collect() instead of a keyed <For/>
|
||||
contacts
|
||||
.into_iter()
|
||||
@@ -126,12 +127,15 @@ pub fn Contact(cx: Scope) -> impl IntoView {
|
||||
get_contact,
|
||||
);
|
||||
|
||||
let contact_display = move || match contact.read() {
|
||||
let contact_display = move || match contact.read(cx) {
|
||||
// None => loading, but will be caught by Suspense fallback
|
||||
// I'm only doing this explicitly for the example
|
||||
None => None,
|
||||
// Some(None) => has loaded and found no contact
|
||||
Some(None) => Some(view! { cx, <p>"No contact with this ID was found."</p> }.into_any()),
|
||||
Some(None) => Some(
|
||||
view! { cx, <p>"No contact with this ID was found."</p> }
|
||||
.into_any(),
|
||||
),
|
||||
// Some(Some) => has loaded and found a contact
|
||||
Some(Some(contact)) => Some(
|
||||
view! { cx,
|
||||
|
||||
@@ -39,7 +39,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
|
||||
let posts =
|
||||
create_resource(cx, || (), |_| async { list_post_metadata().await });
|
||||
let posts_view = move || {
|
||||
posts.with(|posts| posts
|
||||
posts.with(cx, |posts| posts
|
||||
.clone()
|
||||
.map(|posts| {
|
||||
posts.iter()
|
||||
@@ -82,7 +82,7 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||
});
|
||||
|
||||
let post_view = move || {
|
||||
post.with(|post| {
|
||||
post.with(cx, |post| {
|
||||
post.clone().map(|post| {
|
||||
view! { cx,
|
||||
// render content
|
||||
|
||||
@@ -140,7 +140,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read()
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
|
||||
|
||||
@@ -159,7 +159,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read()
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
|
||||
|
||||
@@ -251,11 +251,6 @@ async fn handle_server_fns_inner(
|
||||
res_options_inner.headers.clone(),
|
||||
);
|
||||
|
||||
if let Some(header_ref) = res.headers_mut()
|
||||
{
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some(
|
||||
@@ -285,6 +280,12 @@ async fn handle_server_fns_inner(
|
||||
Some(status) => res.status(status),
|
||||
None => res,
|
||||
};
|
||||
// This must be after the default referrer
|
||||
// redirect so that it overwrites the one above
|
||||
if let Some(header_ref) = res.headers_mut()
|
||||
{
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header(
|
||||
|
||||
@@ -28,7 +28,7 @@ use std::rc::Rc;
|
||||
/// <div>
|
||||
/// <Suspense fallback=move || view! { cx, <p>"Loading (Suspense Fallback)..."</p> }>
|
||||
/// {move || {
|
||||
/// cats.read().map(|data| match data {
|
||||
/// cats.read(cx).map(|data| match data {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
|
||||
/// Some(cats) => view! { cx,
|
||||
/// <div>{
|
||||
@@ -69,7 +69,8 @@ where
|
||||
|
||||
let orig_child = Rc::new(children);
|
||||
|
||||
let current_id = HydrationCtx::peek();
|
||||
let before_me = HydrationCtx::peek();
|
||||
let current_id = HydrationCtx::next_component();
|
||||
|
||||
let child = DynChild::new({
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
@@ -142,5 +143,7 @@ where
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
HydrationCtx::continue_from(before_me);
|
||||
|
||||
leptos_dom::View::Suspense(current_id, core_component)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use std::{cell::RefCell, rc::Rc};
|
||||
/// set_pending=set_pending.into()
|
||||
/// >
|
||||
/// {move || {
|
||||
/// cats.read().map(|data| match data {
|
||||
/// cats.read(cx).map(|data| match data {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
|
||||
/// Some(cats) => view! { cx,
|
||||
/// <div>{
|
||||
|
||||
@@ -48,7 +48,7 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
/// A stable identifer within the server-rendering or hydration process.
|
||||
/// A stable identifier within the server-rendering or hydration process.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct HydrationKey {
|
||||
/// The key of the previous component.
|
||||
|
||||
@@ -760,7 +760,7 @@ pub fn window() -> web_sys::Window {
|
||||
|
||||
/// Returns the [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document).
|
||||
///
|
||||
/// This is cached as a thread-local variable, so calling `window()` multiple times
|
||||
/// This is cached as a thread-local variable, so calling `document()` multiple times
|
||||
/// requires only one call out to JavaScript.
|
||||
pub fn document() -> web_sys::Document {
|
||||
DOCUMENT.with(|document| document.clone())
|
||||
|
||||
@@ -54,11 +54,11 @@ use std::{
|
||||
/// // when we read the signal, it contains either
|
||||
/// // 1) None (if the Future isn't ready yet) or
|
||||
/// // 2) Some(T) (if the future's already resolved)
|
||||
/// assert_eq!(cats(), Some(vec!["1".to_string()]));
|
||||
/// assert_eq!(cats.read(cx), Some(vec!["1".to_string()]));
|
||||
///
|
||||
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
|
||||
/// set_how_many_cats(2);
|
||||
/// assert_eq!(cats(), Some(vec!["2".to_string()]));
|
||||
/// assert_eq!(cats.read(cx), Some(vec!["2".to_string()]));
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
@@ -121,7 +121,6 @@ where
|
||||
let source = create_memo(cx, move |_| source());
|
||||
|
||||
let r = Rc::new(ResourceState {
|
||||
scope: cx,
|
||||
value,
|
||||
set_value,
|
||||
loading,
|
||||
@@ -245,7 +244,6 @@ where
|
||||
let source = create_memo(cx, move |_| source());
|
||||
|
||||
let r = Rc::new(ResourceState {
|
||||
scope: cx,
|
||||
value,
|
||||
set_value,
|
||||
loading,
|
||||
@@ -371,14 +369,14 @@ where
|
||||
/// resource.
|
||||
///
|
||||
/// If you want to get the value without cloning it, use [Resource::with].
|
||||
/// (`value.read()` is equivalent to `value.with(T::clone)`.)
|
||||
pub fn read(&self) -> Option<T>
|
||||
/// (`value.read(cx)` is equivalent to `value.with(cx, T::clone)`.)
|
||||
pub fn read(&self, cx: Scope) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.read()
|
||||
resource.read(cx)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -392,10 +390,10 @@ where
|
||||
///
|
||||
/// If you want to get the value by cloning it, you can use
|
||||
/// [Resource::read].
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.with(f)
|
||||
resource.with(cx, f)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -427,13 +425,16 @@ where
|
||||
/// Returns a [std::future::Future] that will resolve when the resource has loaded,
|
||||
/// yield its [ResourceId] and a JSON string.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub async fn to_serialization_resolver(&self) -> (ResourceId, String)
|
||||
pub async fn to_serialization_resolver(
|
||||
&self,
|
||||
cx: Scope,
|
||||
) -> (ResourceId, String)
|
||||
where
|
||||
T: Serializable,
|
||||
{
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.to_serialization_resolver(self.id)
|
||||
resource.to_serialization_resolver(cx, self.id)
|
||||
})
|
||||
})
|
||||
.expect(
|
||||
@@ -479,11 +480,11 @@ where
|
||||
/// // when we read the signal, it contains either
|
||||
/// // 1) None (if the Future isn't ready yet) or
|
||||
/// // 2) Some(T) (if the future's already resolved)
|
||||
/// assert_eq!(cats(), Some(vec!["1".to_string()]));
|
||||
/// assert_eq!(cats.read(cx), Some(vec!["1".to_string()]));
|
||||
///
|
||||
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
|
||||
/// set_how_many_cats(2);
|
||||
/// assert_eq!(cats(), Some(vec!["2".to_string()]));
|
||||
/// assert_eq!(cats.read(cx), Some(vec!["2".to_string()]));
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
@@ -531,48 +532,12 @@ where
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<S, T> FnOnce<()> for Resource<S, T>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = Option<T>;
|
||||
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<S, T> FnMut<()> for Resource<S, T>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<S, T> Fn<()> for Resource<S, T>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ResourceState<S, T>
|
||||
where
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
scope: Scope,
|
||||
value: ReadSignal<Option<T>>,
|
||||
set_value: WriteSignal<Option<T>>,
|
||||
pub loading: ReadSignal<bool>,
|
||||
@@ -590,15 +555,15 @@ where
|
||||
S: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
pub fn read(&self) -> Option<T>
|
||||
pub fn read(&self, cx: Scope) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.with(T::clone)
|
||||
self.with(cx, T::clone)
|
||||
}
|
||||
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
let suspense_cx = use_context::<SuspenseContext>(self.scope);
|
||||
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
let suspense_cx = use_context::<SuspenseContext>(cx);
|
||||
|
||||
let v = self
|
||||
.value
|
||||
@@ -611,21 +576,23 @@ where
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
if let Some(s) = &suspense_cx {
|
||||
let mut contexts = suspense_contexts.borrow_mut();
|
||||
if !contexts.contains(s) {
|
||||
contexts.insert(*s);
|
||||
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
|
||||
{
|
||||
if !contexts.contains(s) {
|
||||
contexts.insert(*s);
|
||||
|
||||
// on subsequent reads, increment will be triggered in load()
|
||||
// because the context has been tracked here
|
||||
// on the first read, resource is already loading without having incremented
|
||||
if !has_value {
|
||||
s.increment();
|
||||
// on subsequent reads, increment will be triggered in load()
|
||||
// because the context has been tracked here
|
||||
// on the first read, resource is already loading without having incremented
|
||||
if !has_value {
|
||||
s.increment();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
create_isomorphic_effect(self.scope, increment);
|
||||
create_isomorphic_effect(cx, increment);
|
||||
v
|
||||
}
|
||||
|
||||
@@ -685,6 +652,7 @@ where
|
||||
|
||||
pub fn resource_to_serialization_resolver(
|
||||
&self,
|
||||
cx: Scope,
|
||||
id: ResourceId,
|
||||
) -> std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>
|
||||
where
|
||||
@@ -694,7 +662,7 @@ where
|
||||
|
||||
let (tx, mut rx) = futures::channel::mpsc::channel(1);
|
||||
let value = self.value;
|
||||
create_isomorphic_effect(self.scope, move |_| {
|
||||
create_isomorphic_effect(cx, move |_| {
|
||||
value.with({
|
||||
let mut tx = tx.clone();
|
||||
move |value| {
|
||||
@@ -731,6 +699,7 @@ pub(crate) trait SerializableResource {
|
||||
|
||||
fn to_serialization_resolver(
|
||||
&self,
|
||||
cx: Scope,
|
||||
id: ResourceId,
|
||||
) -> Pin<Box<dyn Future<Output = (ResourceId, String)>>>;
|
||||
}
|
||||
@@ -746,9 +715,10 @@ where
|
||||
|
||||
fn to_serialization_resolver(
|
||||
&self,
|
||||
cx: Scope,
|
||||
id: ResourceId,
|
||||
) -> Pin<Box<dyn Future<Output = (ResourceId, String)>>> {
|
||||
let fut = self.resource_to_serialization_resolver(id);
|
||||
let fut = self.resource_to_serialization_resolver(cx, id);
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,11 +421,12 @@ impl Runtime {
|
||||
|
||||
pub(crate) fn serialization_resolvers(
|
||||
&self,
|
||||
cx: Scope,
|
||||
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
let f = FuturesUnordered::new();
|
||||
for (id, resource) in self.resources.borrow().iter() {
|
||||
if let AnyResource::Serializable(resource) = resource {
|
||||
f.push(resource.to_serialization_resolver(id));
|
||||
f.push(resource.to_serialization_resolver(cx, id));
|
||||
}
|
||||
}
|
||||
f
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
console_warn,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
suspense::StreamChunk,
|
||||
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
|
||||
@@ -266,10 +267,13 @@ impl Scope {
|
||||
) {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let scopes = runtime.scopes.borrow();
|
||||
let scope = scopes.get(self.id).expect(
|
||||
"tried to add property to a scope that has been disposed",
|
||||
);
|
||||
f(&mut scope.borrow_mut());
|
||||
if let Some(scope) = scopes.get(self.id) {
|
||||
f(&mut scope.borrow_mut());
|
||||
} else {
|
||||
console_warn(
|
||||
"tried to add property to a scope that has been disposed",
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -353,8 +357,10 @@ impl Scope {
|
||||
pub fn serialization_resolvers(
|
||||
&self,
|
||||
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers())
|
||||
.unwrap_or_default()
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.serialization_resolvers(*self)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
|
||||
@@ -409,7 +415,9 @@ impl Scope {
|
||||
{
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
std::mem::take(&mut shared_context.pending_fragments)
|
||||
let f = std::mem::take(&mut shared_context.pending_fragments);
|
||||
println!("pending_fragments = {}", f.len());
|
||||
f
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ where
|
||||
|
||||
/// Updates whether the action is currently pending.
|
||||
pub fn set_pending(&self, pending: bool) {
|
||||
self.0.with_value(|a| a.pending.set(pending))
|
||||
self.0.try_with_value(|a| a.pending.set(pending));
|
||||
}
|
||||
|
||||
/// The URL associated with the action (typically as part of a server function.)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.2.0-alpha2"
|
||||
version = "0.2.0-beta"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.2.0-alpha2"
|
||||
version = "0.2.0-beta"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -12,7 +12,7 @@ description = "Router for the Leptos web framework."
|
||||
leptos = { workspace = true }
|
||||
cfg-if = "1"
|
||||
common_macros = "0.1"
|
||||
gloo-net = "0.2"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
lazy_static = "1"
|
||||
linear-map = "1"
|
||||
log = "0.4"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{use_navigate, use_resolved_path, ToHref};
|
||||
use crate::{use_navigate, use_resolved_path, ToHref, Url};
|
||||
use leptos::*;
|
||||
use std::{error::Error, rc::Rc};
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::RequestRedirect;
|
||||
|
||||
type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
|
||||
type OnResponse = Rc<dyn Fn(&web_sys::Response)>;
|
||||
@@ -90,12 +91,13 @@ where
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", &enctype)
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(params)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
log::error!("<Form/> error while POSTing: {e:#?}");
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
}
|
||||
@@ -110,15 +112,22 @@ where
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
|
||||
if resp.status() == 303 {
|
||||
if let Some(redirect_url) =
|
||||
resp.headers().get("Location")
|
||||
{
|
||||
_ = navigate(
|
||||
&redirect_url,
|
||||
Default::default(),
|
||||
);
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
request_animation_frame(move || {
|
||||
if let Err(e) = navigate(
|
||||
&url.pathname,
|
||||
Default::default(),
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,7 +216,7 @@ where
|
||||
input.set(Some(data));
|
||||
action.set_pending(true);
|
||||
}
|
||||
Err(e) => log::error!("{e}"),
|
||||
Err(e) => error!("{e}"),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -225,15 +234,19 @@ where
|
||||
.as_string()
|
||||
.expect("couldn't get String from JsString"),
|
||||
) {
|
||||
Ok(res) => value.set(Some(Ok(res))),
|
||||
Err(e) => value.set(Some(Err(
|
||||
ServerFnError::Deserialization(e.to_string()),
|
||||
))),
|
||||
Ok(res) => {
|
||||
value.try_set(Some(Ok(res)));
|
||||
}
|
||||
Err(e) => {
|
||||
value.try_set(Some(Err(
|
||||
ServerFnError::Deserialization(e.to_string()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("{e:?}"),
|
||||
Err(e) => error!("{e:?}"),
|
||||
};
|
||||
input.set(None);
|
||||
input.try_set(None);
|
||||
action.set_pending(false);
|
||||
});
|
||||
});
|
||||
@@ -293,7 +306,7 @@ where
|
||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
let data = action_input_from_form_data(&form_data);
|
||||
match data {
|
||||
Err(e) => log::error!("{e}"),
|
||||
Err(e) => error!("{e}"),
|
||||
Ok(input) => {
|
||||
ev.prevent_default();
|
||||
multi_action.dispatch(input);
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
//! <div>
|
||||
//! // show the contacts
|
||||
//! <ul>
|
||||
//! {move || contacts.read().map(|contacts| view! { cx, <li>"todo contact info"</li> } )}
|
||||
//! {move || contacts.read(cx).map(|contacts| view! { cx, <li>"todo contact info"</li> } )}
|
||||
//! </ul>
|
||||
//!
|
||||
//! // insert the nested child route here
|
||||
|
||||
Reference in New Issue
Block a user