mirror of
https://github.com/leptos-rs/leptos
synced 2024-09-20 06:21:57 +00:00
Merge pull request #338 from benwis/error-handling
ErrorBoundary Component
This commit is contained in:
commit
a5531b1a7c
16 changed files with 199 additions and 37 deletions
28
examples/hackernews_axum/src/error_template.rs
Normal file
28
examples/hackernews_axum/src/error_template.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use leptos::Errors;
|
||||
use leptos::{view, 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
|
||||
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
|
||||
let Some(errors) = errors else {
|
||||
panic!("No Errors found and we expected errors!");
|
||||
};
|
||||
view! {cx,
|
||||
<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()}
|
||||
// a unique key for each item as a reference
|
||||
key=|error| error.0.clone()
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
view! {
|
||||
cx,
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
|
@ -5,22 +5,26 @@ if #[cfg(feature = "ssr")] {
|
|||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::LeptosOptions;
|
||||
|
||||
pub async fn file_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await?;
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
match res.status() {
|
||||
StatusCode::OK => Ok(res),
|
||||
_ => Err((res.status(), "File Not Found".to_string()))
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,5 +41,7 @@ if #[cfg(feature = "ssr")] {
|
|||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -3,7 +3,8 @@ use leptos::{component, view, IntoView, Scope};
|
|||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod file;
|
||||
pub mod error_template;
|
||||
pub mod fallback;
|
||||
pub mod handlers;
|
||||
mod routes;
|
||||
use routes::nav::*;
|
||||
|
|
|
@ -11,7 +11,7 @@ if #[cfg(feature = "ssr")] {
|
|||
};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
use hackernews_axum::file::file_handler;
|
||||
use hackernews_axum::fallback::file_and_error_handler;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
@ -26,9 +26,9 @@ if #[cfg(feature = "ssr")] {
|
|||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(file_handler))
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
|
||||
.fallback(file_handler)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
|
||||
// run our app with hyper
|
||||
|
|
|
@ -38,17 +38,7 @@ sqlx = { version = "0.6.2", features = [
|
|||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"leptos_axum",
|
||||
]
|
||||
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "leptos_axum"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
|
@ -103,4 +93,4 @@ lib-features = ["hydrate"]
|
|||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
lib-default-features = false
|
||||
|
|
|
@ -28,7 +28,7 @@ cargo leptos build --release
|
|||
## Server Side Rendering without cargo-leptos
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"pkg"`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
|
|
28
examples/todo_app_sqlite_axum/src/error_template.rs
Normal file
28
examples/todo_app_sqlite_axum/src/error_template.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use leptos::Errors;
|
||||
use leptos::{view, 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
|
||||
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
|
||||
let Some(errors) = errors else {
|
||||
panic!("No Errors found and we expected errors!");
|
||||
};
|
||||
view! {cx,
|
||||
<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()}
|
||||
// a unique key for each item as a reference
|
||||
key=|error| error.0.clone()
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
view! {
|
||||
cx,
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
|
@ -5,22 +5,26 @@ if #[cfg(feature = "ssr")] {
|
|||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::LeptosOptions;
|
||||
|
||||
pub async fn file_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await?;
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
match res.status() {
|
||||
StatusCode::OK => Ok(res),
|
||||
_ => Err((res.status(), "File Not Found".to_string()))
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,5 +41,7 @@ if #[cfg(feature = "ssr")] {
|
|||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod file;
|
||||
pub mod error_template;
|
||||
pub mod fallback;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
|
|
|
@ -10,7 +10,7 @@ if #[cfg(feature = "ssr")] {
|
|||
};
|
||||
use crate::todo::*;
|
||||
use todo_app_sqlite_axum::*;
|
||||
use crate::file::file_handler;
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -36,7 +36,7 @@ if #[cfg(feature = "ssr")] {
|
|||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
|
||||
.fallback(file_handler)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
|
||||
// run our app with hyper
|
||||
|
|
|
@ -120,8 +120,9 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
|||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
<Todos/>
|
||||
}/>
|
||||
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
|
27
leptos/src/error_boundary.rs
Normal file
27
leptos/src/error_boundary.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use leptos_dom::{Errors, Fragment, IntoView, View};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
|
||||
|
||||
#[component(transparent)]
|
||||
pub fn ErrorBoundary<F>(
|
||||
cx: Scope,
|
||||
/// The components inside the tag which will get rendered
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
/// A fallback that will be shown if an error occurs.
|
||||
fallback: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope, Option<RwSignal<Errors>>) -> View + 'static,
|
||||
{
|
||||
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
|
||||
|
||||
provide_context(cx, errors);
|
||||
|
||||
// Run children so that they render and execute resources
|
||||
let children = children(cx);
|
||||
|
||||
move || match errors.get().0.is_empty() {
|
||||
true => children.clone(),
|
||||
false => fallback(cx, Some(errors)).into(),
|
||||
}
|
||||
}
|
|
@ -138,7 +138,8 @@ pub use leptos_server::*;
|
|||
|
||||
pub use tracing;
|
||||
pub use typed_builder;
|
||||
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
mod for_loop;
|
||||
pub use for_loop::*;
|
||||
mod suspense;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod dyn_child;
|
||||
mod each;
|
||||
mod errors;
|
||||
mod fragment;
|
||||
mod unit;
|
||||
|
||||
|
@ -11,6 +12,7 @@ use crate::{
|
|||
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
|
||||
pub use dyn_child::*;
|
||||
pub use each::*;
|
||||
pub use errors::*;
|
||||
pub use fragment::*;
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
|
|
71
leptos_dom/src/components/errors.rs
Normal file
71
leptos_dom/src/components/errors.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use crate::{HydrationCtx, HydrationKey, IntoView};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::{use_context, RwSignal};
|
||||
use std::{collections::HashMap, error::Error, rc::Rc};
|
||||
|
||||
/// A struct to hold all the possible errors that could be provided by child Views
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Errors(pub HashMap<HydrationKey, Rc<dyn Error>>);
|
||||
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
match self {
|
||||
Ok(stuff) => stuff.into_view(cx),
|
||||
Err(error) => {
|
||||
match use_context::<RwSignal<Errors>>(cx) {
|
||||
Some(errors) => {
|
||||
let id = HydrationCtx::id();
|
||||
errors.update({
|
||||
let id = id.clone();
|
||||
move |errors: &mut Errors| errors.insert(id, error)
|
||||
});
|
||||
|
||||
// remove the error from the list if this drops,
|
||||
// i.e., if it's in a DynChild that switches from Err to Ok
|
||||
// Only can run on the client, will panic on the server
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "hydrate", feature="csr"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove::<E>(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
warn!(
|
||||
"No ErrorBoundary components found! Returning errors will not \
|
||||
be handled and will silently disappear"
|
||||
);
|
||||
}
|
||||
}
|
||||
().into_view(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Errors {
|
||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: HydrationKey, error: E)
|
||||
where
|
||||
E: Error + 'static,
|
||||
{
|
||||
self.0.insert(key, Rc::new(error));
|
||||
}
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove<E>(&mut self, key: &HydrationKey)
|
||||
where
|
||||
E: Error + 'static,
|
||||
{
|
||||
self.0.remove(key);
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ cfg_if! {
|
|||
}
|
||||
|
||||
/// A stable identifer within the server-rendering or hydration process.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct HydrationKey {
|
||||
/// The key of the previous component.
|
||||
pub previous: String,
|
||||
|
|
Loading…
Reference in a new issue