diff --git a/examples/errors_axum/src/error_template.rs b/examples/errors_axum/src/error_template.rs index 425e29fff..4929180dc 100644 --- a/examples/errors_axum/src/error_template.rs +++ b/examples/errors_axum/src/error_template.rs @@ -1,8 +1,6 @@ use crate::errors::AppError; use cfg_if::cfg_if; -use leptos::Errors; -use leptos::*; - +use leptos::{Errors, *}; #[cfg(feature = "ssr")] use leptos_axum::ResponseOptions; @@ -23,12 +21,11 @@ pub fn ErrorTemplate( }; // Get Errors from Signal - let errors = errors.get().0; - // Downcast lets us take a type that implements `std::error::Error` let errors: Vec = errors + .get() .into_iter() - .filter_map(|(_k, v)| v.downcast_ref::().cloned()) + .filter_map(|(_, v)| v.downcast_ref::().cloned()) .collect(); log!("Errors: {errors:#?}"); @@ -47,7 +44,7 @@ pub fn ErrorTemplate( // a function that returns the items we're iterating over; a signal is fine each= move || {errors.clone().into_iter().enumerate()} // a unique key for each item as a reference - key=|(index, _error)| *index + key=|(index, _)| *index // renders each item to a view view=move |cx, error| { let error_string = error.1.to_string(); diff --git a/examples/fetch/index.html b/examples/fetch/index.html index 12c1688d5..bb3d81b74 100644 --- a/examples/fetch/index.html +++ b/examples/fetch/index.html @@ -9,6 +9,12 @@ max-width: 250px; height: auto; } + + .error { + border: 1px solid red; + color: red; + background-color: lightpink; + } \ No newline at end of file diff --git a/examples/fetch/src/lib.rs b/examples/fetch/src/lib.rs index 51757265d..61dd1b9bd 100644 --- a/examples/fetch/src/lib.rs +++ b/examples/fetch/src/lib.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use leptos::*; use serde::{Deserialize, Serialize}; @@ -6,18 +7,18 @@ pub struct Cat { url: String, } -async fn fetch_cats(count: u32) -> Result, ()> { +async fn fetch_cats(count: u32) -> Result> { if count > 0 { + // make the request let res = reqwasm::http::Request::get(&format!( - "https://api.thecatapi.com/v1/images/search?limit={}", - count + "https://api.thecatapi.com/v1/images/search?limit={count}", )) .send() - .await - .map_err(|_| ())? + .await? + // convert it to JSON .json::>() - .await - .map_err(|_| ())? + .await? + // extract the URL field for each cat .into_iter() .map(|cat| cat.url) .collect::>(); @@ -29,9 +30,45 @@ async fn fetch_cats(count: u32) -> Result, ()> { pub fn fetch_example(cx: Scope) -> impl IntoView { let (cat_count, set_cat_count) = create_signal::(cx, 1); - let cats = create_resource(cx, cat_count, |count| fetch_cats(count)); - view! { cx, + // we use local_resource here because + // 1) anyhow::Result isn't serializable/deserializable + // 2) we're not doing server-side rendering in this example anyway + // (during SSR, create_resource will begin loading on the server and resolve on the client) + let cats = create_local_resource(cx, cat_count, fetch_cats); + + let fallback = move |cx, errors: RwSignal| { + let error_list = move || { + errors.with(|errors| { + errors + .iter() + .map(|(_, e)| view! { cx,
  • {e.to_string()}
  • }) + .collect::>() + }) + }; + + view! { cx, +
    +

    "Error"

    +
      {error_list}
    +
    + } + }; + + // the renderer can handle Option<_> and Result<_> states + // by displaying nothing for None if the resource is still loading + // 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| { + data.iter() + .flatten() + .map(|cat| view! { cx, }) + .collect::>() + }) + }; + + view! { cx,
    - "Loading (Suspense Fallback)..."
    }> - {move || { - cats.read().map(|data| match data { - Err(_) => view! { cx,
    "Error"
    }.into_view(cx), - Ok(cats) => view! { cx, -
    { - cats.iter() - .map(|src| { - view! { cx, - - } - }) - .collect::>() - }
    - }.into_view(cx), - }) - } - } - + + "Loading (Suspense Fallback)..."}> + {cats_view} + + } } diff --git a/examples/hackernews_axum/src/error_template.rs b/examples/hackernews_axum/src/error_template.rs index e0a001f7a..a8b85d533 100644 --- a/examples/hackernews_axum/src/error_template.rs +++ b/examples/hackernews_axum/src/error_template.rs @@ -1,5 +1,4 @@ -use leptos::Errors; -use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View}; +use leptos::{view, Errors, For, ForProps, IntoView, RwSignal, Scope, View}; // A basic function to display errors served by the error boundaries. Feel free to do more complicated things // here than just displaying them @@ -11,12 +10,12 @@ pub fn error_template(cx: Scope, errors: Option>) -> View {

    "Errors"

    "Error: " {error_string}

    diff --git a/examples/todo_app_sqlite_axum/src/error_template.rs b/examples/todo_app_sqlite_axum/src/error_template.rs index bded6bc1a..c42f64102 100644 --- a/examples/todo_app_sqlite_axum/src/error_template.rs +++ b/examples/todo_app_sqlite_axum/src/error_template.rs @@ -1,8 +1,6 @@ use crate::errors::TodoAppError; use cfg_if::cfg_if; -use leptos::Errors; -use leptos::*; - +use leptos::{Errors, *}; #[cfg(feature = "ssr")] use leptos_axum::ResponseOptions; @@ -23,14 +21,12 @@ pub fn ErrorTemplate( }; // Get Errors from Signal - let errors = errors.get().0; - // Downcast lets us take a type that implements `std::error::Error` let errors: Vec = errors + .get() .into_iter() - .filter_map(|(_k, v)| v.downcast_ref::().cloned()) + .filter_map(|(_, v)| v.downcast_ref::().cloned()) .collect(); - println!("Errors: {errors:#?}"); // Only the response code for the first error is actually sent from the server // this may be customized by the specific application diff --git a/leptos/src/error_boundary.rs b/leptos/src/error_boundary.rs index 051ec0105..344119b2a 100644 --- a/leptos/src/error_boundary.rs +++ b/leptos/src/error_boundary.rs @@ -46,15 +46,15 @@ where let children = children(cx); move || { - match errors.get().0.is_empty() { - true => children.clone().into_view(cx), - false => view! { cx, - <> - {fallback(cx, errors)} - {children.clone()} - + match errors.with(Errors::is_empty) { + true => children.clone().into_view(cx), + false => view! { cx, + <> + {fallback(cx, errors)} + {children.clone()} + + } + .into_view(cx), } - .into_view(cx), - } } } diff --git a/leptos_dom/src/components/errors.rs b/leptos_dom/src/components/errors.rs index 98cd3a7ab..8562f7908 100644 --- a/leptos_dom/src/components/errors.rs +++ b/leptos_dom/src/components/errors.rs @@ -5,7 +5,66 @@ use std::{collections::HashMap, error::Error, sync::Arc}; /// A struct to hold all the possible errors that could be provided by child Views #[derive(Debug, Clone, Default)] -pub struct Errors(pub HashMap>); +pub struct Errors(HashMap>); + +/// A unique key for an error that occurs at a particular location in the user interface. +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct ErrorKey(String); + +impl From for ErrorKey +where + T: Into, +{ + fn from(key: T) -> ErrorKey { + ErrorKey(key.into()) + } +} + +impl IntoIterator for Errors { + type Item = (ErrorKey, Arc); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter(self.0.into_iter()) + } +} + +/// An owning iterator over all the errors contained in the [Errors] struct. +pub struct IntoIter( + std::collections::hash_map::IntoIter< + ErrorKey, + Arc, + >, +); + +impl Iterator for IntoIter { + type Item = (ErrorKey, Arc); + + fn next( + &mut self, + ) -> std::option::Option<::Item> { + self.0.next() + } +} + +/// An iterator over all the errors contained in the [Errors] struct. +pub struct Iter<'a>( + std::collections::hash_map::Iter< + 'a, + ErrorKey, + Arc, + >, +); + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a ErrorKey, &'a Arc); + + fn next( + &mut self, + ) -> std::option::Option<::Item> { + self.0.next() + } +} impl IntoView for Result where @@ -13,7 +72,7 @@ where E: Error + Send + Sync + 'static, { fn into_view(self, cx: leptos_reactive::Scope) -> crate::View { - let id = HydrationCtx::peek().previous; + let id = ErrorKey(HydrationCtx::peek().previous); let errors = use_context::>(cx); match self { Ok(stuff) => { @@ -67,22 +126,37 @@ where } } impl Errors { + /// Returns `true` if there are no errors. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Add an error to Errors that will be processed by `` - pub fn insert(&mut self, key: String, error: E) + pub fn insert(&mut self, key: ErrorKey, error: E) where E: Error + Send + Sync + 'static, { self.0.insert(key, Arc::new(error)); } + /// Add an error with the default key for errors outside the reactive system pub fn insert_with_default_key(&mut self, error: E) where E: Error + Send + Sync + 'static, { - self.0.insert(String::new(), Arc::new(error)); + self.0.insert(Default::default(), Arc::new(error)); } + /// Remove an error to Errors that will be processed by `` - pub fn remove(&mut self, key: &str) { - self.0.remove(key); + pub fn remove( + &mut self, + key: &ErrorKey, + ) -> Option> { + self.0.remove(key) + } + + /// An iterator over all the errors, in arbitrary order. + pub fn iter(&self) -> Iter<'_> { + Iter(self.0.iter()) } } diff --git a/leptos_reactive/src/resource.rs b/leptos_reactive/src/resource.rs index e501273c2..b5c92f0c2 100644 --- a/leptos_reactive/src/resource.rs +++ b/leptos_reactive/src/resource.rs @@ -509,8 +509,8 @@ slotmap::new_key_type! { impl Clone for Resource where - S: Clone + 'static, - T: Clone + 'static, + S: 'static, + T: 'static, { fn clone(&self) -> Self { Self { @@ -526,8 +526,8 @@ where impl Copy for Resource where - S: Clone + 'static, - T: Clone + 'static, + S: 'static, + T: 'static, { }