mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 11:04:40 -05:00
change: tweak API of Errors and implement IntoIter (#522)
This commit is contained in:
@@ -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<AppError> = errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.filter_map(|(_, v)| v.downcast_ref::<AppError>().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();
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
max-width: 250px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
border: 1px solid red;
|
||||
color: red;
|
||||
background-color: lightpink;
|
||||
}
|
||||
</style>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -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<Vec<String>, ()> {
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
|
||||
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::<Vec<Cat>>()
|
||||
.await
|
||||
.map_err(|_| ())?
|
||||
.await?
|
||||
// extract the URL field for each cat
|
||||
.into_iter()
|
||||
.map(|cat| cat.url)
|
||||
.collect::<Vec<_>>();
|
||||
@@ -29,9 +30,45 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
|
||||
pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(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<Errors>| {
|
||||
let error_list = move || {
|
||||
errors.with(|errors| {
|
||||
errors
|
||||
.iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div class="error">
|
||||
<h2>"Error"</h2>
|
||||
<ul>{error_list}</ul>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
// 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, <img src={cat}/> })
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<label>
|
||||
"How many cats would you like?"
|
||||
@@ -43,25 +80,11 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
|
||||
{move || {
|
||||
cats.read().map(|data| match data {
|
||||
Err(_) => view! { cx, <pre>"Error"</pre> }.into_view(cx),
|
||||
Ok(cats) => view! { cx,
|
||||
<div>{
|
||||
cats.iter()
|
||||
.map(|src| {
|
||||
view! { cx,
|
||||
<img src={src}/>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}</div>
|
||||
}.into_view(cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
</Transition>
|
||||
<ErrorBoundary fallback>
|
||||
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
|
||||
{cats_view}
|
||||
</Transition>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RwSignal<Errors>>) -> View {
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.get().0.into_iter()}
|
||||
each=errors
|
||||
// a unique key for each item as a reference
|
||||
key=|error| error.0.clone()
|
||||
key=|(key, _)| key.clone()
|
||||
// renders each item to a view
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
view= move |cx, (_, error)| {
|
||||
let error_string = error.to_string();
|
||||
view! {
|
||||
cx,
|
||||
<p>"Error: " {error_string}</p>
|
||||
|
||||
@@ -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<TodoAppError> = errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().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
|
||||
|
||||
@@ -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)}
|
||||
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
|
||||
</>
|
||||
match errors.with(Errors::is_empty) {
|
||||
true => children.clone().into_view(cx),
|
||||
false => view! { cx,
|
||||
<>
|
||||
{fallback(cx, errors)}
|
||||
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
|
||||
</>
|
||||
}
|
||||
.into_view(cx),
|
||||
}
|
||||
.into_view(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Arc<dyn Error + Send + Sync>>);
|
||||
pub struct Errors(HashMap<ErrorKey, Arc<dyn Error + Send + Sync>>);
|
||||
|
||||
/// 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<T> From<T> for ErrorKey
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from(key: T) -> ErrorKey {
|
||||
ErrorKey(key.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Errors {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
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<dyn Error + Send + Sync>,
|
||||
>,
|
||||
);
|
||||
|
||||
impl Iterator for IntoIter {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> std::option::Option<<Self as std::iter::Iterator>::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<dyn Error + Send + Sync>,
|
||||
>,
|
||||
);
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = (&'a ErrorKey, &'a Arc<dyn Error + Send + Sync>);
|
||||
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
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::<RwSignal<Errors>>(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 `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: String, error: E)
|
||||
pub fn insert<E>(&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<E>(&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 `<ErrorBoundary/>`
|
||||
pub fn remove(&mut self, key: &str) {
|
||||
self.0.remove(key);
|
||||
pub fn remove(
|
||||
&mut self,
|
||||
key: &ErrorKey,
|
||||
) -> Option<Arc<dyn Error + Send + Sync>> {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
/// An iterator over all the errors, in arbitrary order.
|
||||
pub fn iter(&self) -> Iter<'_> {
|
||||
Iter(self.0.iter())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,8 +509,8 @@ slotmap::new_key_type! {
|
||||
|
||||
impl<S, T> Clone for Resource<S, T>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
@@ -526,8 +526,8 @@ where
|
||||
|
||||
impl<S, T> Copy for Resource<S, T>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user