change: tweak API of Errors and implement IntoIter (#522)

This commit is contained in:
Greg Johnston
2023-02-15 14:03:16 -05:00
committed by GitHub
parent bde585dc3e
commit 00a796d204
8 changed files with 162 additions and 67 deletions

View File

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

View File

@@ -9,6 +9,12 @@
max-width: 250px;
height: auto;
}
.error {
border: 1px solid red;
color: red;
background-color: lightpink;
}
</style>
<body></body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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