Compare commits

..

2 Commits

Author SHA1 Message Date
Greg Johnston
55ea00afdd Router sets status code 404 when it can't match a route and returns fallback 2023-01-16 20:50:48 -05:00
Greg Johnston
f7d5567a35 Fix fallback (signature and functionality) 2023-01-16 19:55:32 -05:00
11 changed files with 132 additions and 86 deletions

View File

@@ -259,20 +259,22 @@ where
let options = options.clone();
let app_fn = app_fn.clone();
let res_options = ResponseOptions::default();
let status = RouterStatusContext::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
let status = status.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
provide_contexts(cx, &req, res_options, status);
(app_fn)(cx).into_view(cx)
}
};
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options).await
stream_app(app, head, tail, res_options, status).await
}
})
}
@@ -336,6 +338,7 @@ where
let app_fn = app_fn.clone();
let data_fn = data_fn.clone();
let res_options = ResponseOptions::default();
let status = RouterStatusContext::default();
async move {
let data = match data_fn(req.clone()).await {
@@ -347,24 +350,31 @@ where
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
let status = status.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
provide_contexts(cx, &req, res_options, status);
(app_fn)(cx, data).into_view(cx)
}
};
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options).await
stream_app(app, head, tail, res_options, status).await
}
})
}
fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseOptions) {
fn provide_contexts(
cx: leptos::Scope,
req: &HttpRequest,
res_options: ResponseOptions,
status: RouterStatusContext,
) {
let path = leptos_corrected_path(req);
let integration = ServerIntegration { path };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, status);
provide_context(cx, MetaContext::new());
provide_context(cx, res_options);
provide_context(cx, req.clone());
@@ -385,6 +395,7 @@ async fn stream_app(
head: String,
tail: String,
res_options: ResponseOptions,
router_status: RouterStatusContext,
) -> HttpResponse<BoxBody> {
let (stream, runtime, _) = render_to_stream_with_prefix_undisposed(app, move |cx| {
let head = use_context::<MetaContext>(cx)
@@ -411,7 +422,15 @@ async fn stream_app(
let res_options = res_options.0.read().await;
let (status, mut headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let status = status.unwrap_or_else(|| {
router_status
.status
.read()
.ok()
.and_then(|s| s.map(|s| StatusCode::from_u16(s).ok()))
.flatten()
.unwrap_or_default()
});
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),

View File

@@ -318,6 +318,8 @@ where
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
let router_status = RouterStatusContext::default();
let router_status2 = router_status.clone();
async move {
// Need to get the path and query string of the Request
@@ -403,6 +405,7 @@ where
cx,
RouterIntegrationContext::new(integration),
);
provide_context(cx, router_status2);
provide_context(cx, MetaContext::new());
provide_context(cx, req_parts);
provide_context(cx, default_res_options);
@@ -471,9 +474,16 @@ where
Box::pin(complete_stream) as PinnedHtmlStream
));
if let Some(status) = res_options.status {
*res.status_mut() = status
}
let status = res_options.status.unwrap_or_else(|| {
router_status
.status
.read()
.ok()
.and_then(|s| s.map(|s| StatusCode::from_u16(s).ok()))
.flatten()
.unwrap_or_default()
});
*res.status_mut() = status;
let mut res_headers = res_options.headers.clone();
res.headers_mut().extend(res_headers.drain());

View File

@@ -16,7 +16,7 @@ fn simple_ssr_test() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span id=\"_0-3\">Value: <!--hk=_0-4o|leptos-dyn-child-start-->0<!--hk=_0-4c|leptos-dyn-child-end-->!</span><button id=\"_0-5\">+1</button></div>"
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span id=\"_0-3\">Value: <leptos-dyn-child-start leptos id=\"_0-4o\"></leptos-dyn-child-start>0<leptos-dyn-child-end leptos id=\"_0-4c\"></leptos-dyn-child-end>!</span><button id=\"_0-5\">+1</button></div>"
);
});
}
@@ -50,7 +50,7 @@ fn ssr_test_with_components() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div class=\"counters\" id=\"_0-1\"><!--hk=_0-1-0o|leptos-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-counter-end--><!--hk=_0-1-5-0o|leptos-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-counter-end--></div>"
"<div class=\"counters\" id=\"_0-1\"><leptos-counter-start leptos id=\"_0-1-0o\"></leptos-counter-start><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <leptos-dyn-child-start leptos id=\"_0-1-4o\"></leptos-dyn-child-start>1<leptos-dyn-child-end leptos id=\"_0-1-4c\"></leptos-dyn-child-end>!</span><button id=\"_0-1-5\">+1</button></div><leptos-counter-end leptos id=\"_0-1-0c\"></leptos-counter-end><leptos-counter-start leptos id=\"_0-1-5-0o\"></leptos-counter-start><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <leptos-dyn-child-start leptos id=\"_0-1-5-4o\"></leptos-dyn-child-start>2<leptos-dyn-child-end leptos id=\"_0-1-5-4c\"></leptos-dyn-child-end>!</span><button id=\"_0-1-5-5\">+1</button></div><leptos-counter-end leptos id=\"_0-1-5-0c\"></leptos-counter-end></div>"
);
});
}

View File

@@ -39,7 +39,6 @@ features = [
"Range",
"Text",
"HtmlCollection",
"TreeWalker",
# Events we cast to in leptos_macro -- added here so we don't force users to import them
"AnimationEvent",

View File

@@ -1,51 +1,21 @@
use cfg_if::cfg_if;
use std::{cell::RefCell, fmt::Display};
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use once_cell::unsync::Lazy as LazyCell;
use std::collections::HashMap;
use wasm_bindgen::JsCast;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use once_cell::unsync::Lazy as LazyCell;
// We can tell if we start in hydration mode by checking to see if the
// id "_0-0-0" is present in the DOM. If it is, we know we are hydrating from
// the server, if not, we are starting off in CSR
thread_local! {
static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
let document = crate::document();
let body = document.body().unwrap();
let walker = document
.create_tree_walker_with_what_to_show(&body, 128)
.unwrap();
let mut map = HashMap::new();
while let Ok(Some(node)) = walker.next_node() {
if let Some(content) = node.text_content() {
if let Some(hk) = content.strip_prefix("hk=") {
if let Some(hk) = hk.split("|").next() {
map.insert(hk.into(), node.unchecked_into());
}
}
}
}
map
});
// We can tell if we start in hydration mode by checking to see if the
// id "_0-0-0" is present in the DOM. If it is, we know we are hydrating from
// the server, if not, we are starting off in CSR
#[cfg(all(target_arch = "wasm32", feature = "web"))]
thread_local! {
static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
#[cfg(debug_assertions)]
return crate::document().get_element_by_id("_0-0-0").is_some()
|| crate::document().get_element_by_id("_0-0-0o").is_some();
static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
#[cfg(debug_assertions)]
return crate::document().get_element_by_id("_0-0-0").is_some()
|| crate::document().get_element_by_id("_0-0-0o").is_some()
|| HYDRATION_COMMENTS.with(|comments| comments.get("_0-0-0o").is_some());
#[cfg(not(debug_assertions))]
return crate::document().get_element_by_id("_0-0-0").is_some()
|| HYDRATION_COMMENTS.with(|comments| comments.get("_0-0-0").is_some());
}));
}
pub(crate) fn get_marker(id: &str) -> Option<web_sys::Comment> {
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
}
}
#[cfg(not(debug_assertions))]
return crate::document().get_element_by_id("_0-0-0").is_some();
}));
}
/// A stable identifer within the server-rendering or hydration process.

View File

@@ -303,7 +303,7 @@ impl Comment {
if HydrationCtx::is_hydrating() {
let id = HydrationCtx::to_string(id, closing);
if let Some(marker) = hydration::get_marker(&id) {
if let Some(marker) = document().get_element_by_id(&id) {
marker.before_with_node_1(&node).unwrap();
marker.remove();
@@ -548,7 +548,7 @@ impl View {
pub fn on<E: ev::EventDescriptor + 'static>(
self,
event: E,
#[allow(unused_mut)] mut event_handler: impl FnMut(E::EventType) + 'static,
mut event_handler: impl FnMut(E::EventType) + 'static,
) -> Self {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {

View File

@@ -16,7 +16,7 @@ use std::borrow::Cow;
/// <p>"Hello, world!"</p>
/// });
/// // static HTML includes some hydration info
/// assert_eq!(html, "<p id=\"_0-1\">Hello, world!</p>");
/// assert_eq!(html, "<style>[leptos]{display:none;}</style><p id=\"_0-1\">Hello, world!</p>");
/// # }}
/// ```
pub fn render_to_string<F, N>(f: F) -> String
@@ -33,7 +33,13 @@ where
runtime.dispose();
html.into()
#[cfg(debug_assertions)]
{
format!("<style>[leptos]{{display:none;}}</style>{html}")
}
#[cfg(not(debug_assertions))]
format!("<style>l-m{{display:none;}}</style>{html}")
}
/// Renders a function to a stream of HTML strings.
@@ -116,6 +122,16 @@ pub fn render_to_stream_with_prefix_undisposed(
let pending_resources = serde_json::to_string(&resources).unwrap();
let prefix = prefix(cx);
let shell = {
#[cfg(debug_assertions)]
{
format!("<style>[leptos]{{display:none;}}</style>{shell}")
}
#[cfg(not(debug_assertions))]
format!("<style>l-m{{display:none;}}</style>{shell}")
};
(
shell,
prefix,
@@ -202,7 +218,7 @@ impl View {
};
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
format!(r#"<leptos-{name}-start leptos id="{}"></leptos-{name}-start>{}<leptos-{name}-end leptos id="{}"></leptos-{name}-end>"#,
HydrationCtx::to_string(&node.id, false),
content(),
HydrationCtx::to_string(&node.id, true),
@@ -210,7 +226,7 @@ impl View {
).into()
} else {
format!(
r#"{}<!--hk={}-->"#,
r#"{}<l-m id="{}"></l-m>"#,
content(),
HydrationCtx::to_string(&node.id, true)
).into()
@@ -227,14 +243,14 @@ impl View {
#[cfg(debug_assertions)]
{
format!(
"<!--hk={}|leptos-unit-->",
"<leptos-unit leptos id={}></leptos-unit>",
HydrationCtx::to_string(&u.id, true)
)
.into()
}
#[cfg(not(debug_assertions))]
format!("<!--hk={}-->", HydrationCtx::to_string(&u.id, true))
format!("<l-m id={}></l-m>", HydrationCtx::to_string(&u.id, true))
.into()
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
),
@@ -270,6 +286,7 @@ impl View {
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
@@ -286,8 +303,10 @@ impl View {
#[cfg(debug_assertions)]
{
format!(
"<!--hk={}|leptos-each-item-start-->{}<!\
--hk={}|leptos-each-item-end-->",
"<leptos-each-item-start leptos \
id=\"{}\"></\
leptos-each-item-start>{}<leptos-each-item-end \
leptos id=\"{}\"></leptos-each-item-end>",
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
@@ -296,7 +315,7 @@ impl View {
#[cfg(not(debug_assertions))]
format!(
"{}<!--hk={}-->",
"{}<l-m id=\"{}\"></l-m>",
content(),
HydrationCtx::to_string(&id, true)
)
@@ -312,7 +331,7 @@ impl View {
cfg_if! {
if #[cfg(debug_assertions)] {
format!(
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
r#"<leptos-{name}-start leptos id="{}"></leptos-{name}-start>{}<leptos-{name}-end leptos id="{}"></leptos-{name}-end>"#,
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
@@ -321,7 +340,7 @@ impl View {
let _ = name;
format!(
r#"{}<!--hk={}-->"#,
r#"{}<l-m id="{}"></l-m>"#,
content(),
HydrationCtx::to_string(&id, true)
).into()

View File

@@ -208,10 +208,10 @@ impl MetaContext {
/// // `app` contains only the body content w/ hydration stuff, not the meta tags
/// assert_eq!(
/// app.into_view(cx).render_to_string(cx),
/// "<main id=\"_0-1\"><!--hk=_0-2c|leptos-unit--><!--hk=_0-4c|leptos-unit--><p id=\"_0-5\">Some text</p></main>"
/// "<main id=\"_0-1\"><leptos-unit leptos id=_0-2c></leptos-unit><leptos-unit leptos id=_0-4c></leptos-unit><p id=\"_0-5\">Some text</p></main>"
/// );
/// // `MetaContext::dehydrate()` gives you HTML that should be in the `<head>`
/// assert_eq!(use_head(cx).dehydrate(), "<title>my title</title><link id=\"leptos-link-1\" href=\"/style.css\" rel=\"stylesheet\" leptos-hk=\"_0-3\"/>")
/// assert_eq!(use_head(cx).dehydrate(), r#"<title>my title</title><link id="leptos-link-1" href="/style.css" rel="stylesheet" leptos-hk="_0-3"/>"#)
/// });
/// # }
/// ```

View File

@@ -138,7 +138,7 @@ impl RouteContext {
self.inner.params
}
pub(crate) fn base(cx: Scope, path: &str, fallback: Option<fn() -> View>) -> Self {
pub(crate) fn base(cx: Scope, path: &str, fallback: Option<fn(Scope) -> View>) -> Self {
Self {
inner: Rc::new(RouteContextInner {
cx,
@@ -148,7 +148,7 @@ impl RouteContext {
path: path.to_string(),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
outlet: Box::new(move || fallback.map(|f| f().into_view(cx))),
outlet: Box::new(move || fallback.as_ref().map(move |f| f(cx))),
}),
}
}

View File

@@ -1,5 +1,9 @@
use cfg_if::cfg_if;
use std::{cell::RefCell, rc::Rc};
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, RwLock},
};
use leptos::*;
use thiserror::Error;
@@ -28,7 +32,7 @@ pub fn Router(
base: Option<&'static str>,
/// A fallback that should be shown if no route is matched.
#[prop(optional)]
fallback: Option<fn() -> View>,
fallback: Option<fn(Scope) -> View>,
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [Routes](crate::Routes) component somewhere
/// to define and display [Route](crate::Route)s.
@@ -61,6 +65,14 @@ pub(crate) struct RouterContextInner {
set_state: WriteSignal<State>,
}
/// Context type that indicates the status of the last request
/// (i.e., whether it was not found, or had an error.)
#[derive(Debug, Clone, Default)]
pub struct RouterStatusContext {
pub status: Arc<RwLock<Option<u16>>>,
pub message: Arc<RwLock<Option<String>>>,
}
impl std::fmt::Debug for RouterContextInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RouterContextInner")
@@ -80,7 +92,7 @@ impl RouterContext {
pub(crate) fn new(
cx: Scope,
base: Option<&'static str>,
fallback: Option<fn() -> View>,
fallback: Option<fn(Scope) -> View>,
) -> Self {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {

View File

@@ -12,7 +12,7 @@ use crate::{
expand_optionals, get_route_matches, join_paths, Branch, Matcher, RouteDefinition,
RouteMatch,
},
RouteContext, RouterContext,
RouteContext, RouterContext, RouterStatusContext,
};
/// Contains route definitions and manages the actual routing process.
@@ -28,6 +28,7 @@ pub fn Routes(
log::warn!("<Routes/> component should be nested within a <Router/>.");
panic!()
});
let base_route = router.base();
let mut branches = Vec::new();
let id_before = HydrationCtx::peek();
@@ -189,19 +190,35 @@ pub fn Routes(
});
// show the root route
let router_status = use_context::<RouterStatusContext>(cx);
let root = create_memo(cx, move |prev| {
provide_context(cx, route_states);
let router_status = router_status.clone();
route_states.with(|state| {
let root = state.routes.borrow();
let root = root.get(0);
if let Some(route) = root {
provide_context(cx, route.clone());
}
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| route.outlet().into_view(cx))
if state.routes.borrow().is_empty() {
if let Some(status) = router_status {
if let Ok(mut lock) = status.status.write() {
*lock = Some(404);
}
}
Some(base_route.outlet().into_view(cx))
} else {
prev.cloned().unwrap()
if let Some(status) = router_status {
if let Ok(mut lock) = status.status.write() {
*lock = None;
}
}
let root = state.routes.borrow();
let root = root.get(0);
if let Some(route) = root {
provide_context(cx, route.clone());
}
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| route.outlet().into_view(cx))
} else {
prev.cloned().unwrap()
}
}
})
});