Compare commits

..

10 Commits

Author SHA1 Message Date
Greg Johnston
5e84c2a769 docs: add create_resource, <Suspense/>, and <Transition/> 2023-02-20 17:38:55 -05:00
Greg Johnston
cc2b310e4c docs: add example of <ButtonC on:click/> syntax (#558) 2023-02-20 15:41:56 -05:00
Thomas Versteeg
884348d7f8 doc: fix button name in parent_child example (#555) 2023-02-20 14:53:04 -05:00
Greg Johnston
dafd6e51c5 v0.2.0-beta (#557) 2023-02-20 14:52:31 -05:00
Ben Wishovich
322041917d fix issue with redirects in server fns creating multiple Location headers (#550) 2023-02-20 08:55:47 -05:00
Ikko Eltociear Ashimine
a2eaf9b3ee fix: typo in hydration docs(#552)
identifer -> identifier
2023-02-20 07:09:33 -05:00
Chrislearn Young
4032bfc210 fix: document docs typo (#553) 2023-02-20 07:08:51 -05:00
Greg Johnston
4ff08f042b change: pass Scope as argument into Resource::read() and Resource::with() (#542) 2023-02-19 19:52:31 -05:00
Greg Johnston
ce4b0ecbe1 fix: more work on hydration IDs with <Suspense/> (#545) 2023-02-18 21:20:40 -05:00
Greg Johnston
6c31d09eb2 revert PR #538 (#544) 2023-02-18 18:39:08 -05:00
33 changed files with 312 additions and 147 deletions

View File

@@ -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

View File

@@ -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]()

View 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 its 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`
Heres 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 its always possible that your resource is still loading.
2. They take a `Scope` argument. Youll 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>

View 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)
}}
}
```
Thats not _so_ bad, but its 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 its still waiting for resources to load, it shows the `fallback`. When theyve 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 dont need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which well 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>

View File

@@ -0,0 +1,9 @@
# `<Transition/>`
Youll 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, theres [`<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>

View File

@@ -0,0 +1,9 @@
# Working with `async`
So far weve 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, well see how Leptos helps smooth out that process for you.

View File

@@ -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>

View File

@@ -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)

View File

@@ -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}/> })

View File

@@ -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)) => {

View File

@@ -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">

View File

@@ -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>

View File

@@ -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)) => {

View File

@@ -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">

View File

@@ -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>

View File

@@ -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! {

View File

@@ -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,

View File

@@ -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

View File

@@ -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()]

View File

@@ -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()]

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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>{

View File

@@ -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.

View File

@@ -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())

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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.)

View File

@@ -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"

View File

@@ -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"

View File

@@ -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);

View File

@@ -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