mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
Remove route data loaders for now.
This commit is contained in:
@@ -31,46 +31,26 @@ async fn render_app(req: HttpRequest) -> impl Responder {
|
||||
view! { cx, <App/> }
|
||||
};
|
||||
|
||||
let accepts_type = req.headers().get("Accept").map(|h| h.to_str());
|
||||
match accepts_type {
|
||||
// if asks for JSON, send the loader function JSON or 404
|
||||
Some(Ok("application/json")) => {
|
||||
let json = loader_to_json(app).await;
|
||||
let head = r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, { main } from '/pkg/hackernews_client.js'; init().then(main);</script>"#;
|
||||
let tail = "</body></html>";
|
||||
|
||||
let res = if let Some(json) = json {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(json)
|
||||
} else {
|
||||
HttpResponse::NotFound().body(())
|
||||
};
|
||||
|
||||
res
|
||||
}
|
||||
// otherwise, send HTML
|
||||
_ => {
|
||||
let head = r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, { main } from '/pkg/hackernews_client.js'; init().then(main);</script>"#;
|
||||
let tail = "</body></html>";
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").streaming(
|
||||
futures::stream::once(async { head.to_string() })
|
||||
.chain(render_to_stream(move |cx| {
|
||||
let app = app(cx);
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>{app}")
|
||||
}))
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
|
||||
)
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().content_type("text/html").streaming(
|
||||
futures::stream::once(async { head.to_string() })
|
||||
.chain(render_to_stream(move |cx| {
|
||||
let app = app(cx);
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>{app}")
|
||||
}))
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
|
||||
)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
|
||||
@@ -5,7 +5,7 @@ use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{
|
||||
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
|
||||
Loader, ParamsMap, RouterContext,
|
||||
ParamsMap, RouterContext,
|
||||
};
|
||||
|
||||
/// Properties that can be passed to a [Route] component, which describes
|
||||
@@ -25,10 +25,6 @@ where
|
||||
/// that takes a [Scope] and returns an [Element] (like `|cx| view! { cx, <p>"Show this"</p> })`
|
||||
/// or `|cx| view! { cx, <MyComponent/>` } or even, for a component with no props, `MyComponent`).
|
||||
pub element: F,
|
||||
/// A data loader is a function that will be run to begin loading data as soon as you navigate to a route.
|
||||
/// These are run in parallel for all nested routes, to avoid data-fetching waterfalls.
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub loader: Option<Loader>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub children: Option<Box<dyn Fn() -> Vec<RouteDefinition>>>,
|
||||
@@ -44,7 +40,6 @@ where
|
||||
{
|
||||
RouteDefinition {
|
||||
path: props.path,
|
||||
loader: props.loader,
|
||||
children: props.children.map(|c| c()).unwrap_or_default(),
|
||||
element: Rc::new(move |cx| (props.element)(cx).into_child(cx)),
|
||||
}
|
||||
@@ -67,9 +62,7 @@ impl RouteContext {
|
||||
let base = base.path();
|
||||
let RouteMatch { path_match, route } = matcher()?;
|
||||
let PathMatch { path, .. } = path_match;
|
||||
let RouteDefinition {
|
||||
element, loader, ..
|
||||
} = route.key;
|
||||
let RouteDefinition { element, .. } = route.key;
|
||||
let params = create_memo(cx, move |_| {
|
||||
matcher()
|
||||
.map(|matched| matched.path_match.params)
|
||||
@@ -81,7 +74,6 @@ impl RouteContext {
|
||||
cx,
|
||||
base_path: base.to_string(),
|
||||
child: Box::new(child),
|
||||
loader,
|
||||
path,
|
||||
original_path: route.original_path.to_string(),
|
||||
params,
|
||||
@@ -105,18 +97,12 @@ impl RouteContext {
|
||||
self.inner.params
|
||||
}
|
||||
|
||||
/// The data loader for the current route.
|
||||
pub fn loader(&self) -> &Option<Loader> {
|
||||
&self.inner.loader
|
||||
}
|
||||
|
||||
pub(crate) fn base(cx: Scope, path: &str, fallback: Option<fn() -> Element>) -> Self {
|
||||
Self {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
cx,
|
||||
base_path: path.to_string(),
|
||||
child: Box::new(|| None),
|
||||
loader: None,
|
||||
path: path.to_string(),
|
||||
original_path: path.to_string(),
|
||||
params: create_memo(cx, |_| ParamsMap::new()),
|
||||
@@ -145,7 +131,6 @@ pub(crate) struct RouteContextInner {
|
||||
cx: Scope,
|
||||
base_path: String,
|
||||
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
|
||||
pub(crate) loader: Option<Loader>,
|
||||
pub(crate) path: String,
|
||||
pub(crate) original_path: String,
|
||||
pub(crate) params: Memo<ParamsMap>,
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
use std::{any::Any, fmt::Debug, future::Future, rc::Rc};
|
||||
|
||||
use leptos::*;
|
||||
|
||||
use crate::{use_location, use_params_map, use_route, ParamsMap, PinnedFuture, Url};
|
||||
|
||||
// SSR and CSR both do the work in their own environment and return it as a resource
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
pub fn use_loader<T>(cx: Scope) -> Resource<(ParamsMap, Url), T>
|
||||
where
|
||||
T: Clone + Debug + Serializable + 'static,
|
||||
{
|
||||
let route = use_route(cx);
|
||||
let params = use_params_map(cx);
|
||||
let loader = route.loader().clone().unwrap_or_else(|| {
|
||||
debug_warn!(
|
||||
"use_loader() called on a route without a loader: {:?}",
|
||||
route.path()
|
||||
);
|
||||
panic!()
|
||||
});
|
||||
|
||||
let location = use_location(cx);
|
||||
let route = use_route(cx);
|
||||
let url = move || Url {
|
||||
origin: String::default(), // don't care what the origin is for this purpose
|
||||
pathname: route.path().into(), // only use this route path, not all matched routes
|
||||
search: location.search.get(), // reload when any of query string changes
|
||||
hash: String::default(), // hash is only client-side, shouldn't refire
|
||||
};
|
||||
|
||||
let loader = loader.data.clone();
|
||||
|
||||
create_resource(
|
||||
cx,
|
||||
move || (params.get(), url()),
|
||||
move |(params, url)| {
|
||||
let loader = loader.clone();
|
||||
async move {
|
||||
let any_data = (loader.clone())(cx, params, url).await;
|
||||
any_data
|
||||
.as_any()
|
||||
.downcast_ref::<T>()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"use_loader() could not downcast to {:?}",
|
||||
std::any::type_name::<T>(),
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// In hydration mode, only run the loader on the server
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn use_loader<T>(cx: Scope) -> Resource<(ParamsMap, Url), T>
|
||||
where
|
||||
T: Clone + Debug + Serializable + 'static,
|
||||
{
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
use crate::use_query_map;
|
||||
|
||||
let route = use_route(cx);
|
||||
let params = use_params_map(cx);
|
||||
|
||||
let location = use_location(cx);
|
||||
let route = use_route(cx);
|
||||
let url = move || Url {
|
||||
origin: String::default(), // don't care what the origin is for this purpose
|
||||
pathname: route.path().into(), // only use this route path, not all matched routes
|
||||
search: location.search.get(), // reload when any of query string changes
|
||||
hash: String::default(), // hash is only client-side, shouldn't refire
|
||||
};
|
||||
|
||||
create_resource(
|
||||
cx,
|
||||
move || (params.get(), url()),
|
||||
move |(params, url)| async move {
|
||||
let route = use_route(cx);
|
||||
let query = use_query_map(cx);
|
||||
|
||||
let mut opts = web_sys::RequestInit::new();
|
||||
opts.method("GET");
|
||||
let url = format!("{}{}", route.path(), query.get().to_query_string());
|
||||
|
||||
let request = web_sys::Request::new_with_str_and_init(&url, &opts).unwrap_throw();
|
||||
request
|
||||
.headers()
|
||||
.set("Accept", "application/json")
|
||||
.unwrap_throw();
|
||||
|
||||
let window = web_sys::window().unwrap_throw();
|
||||
let resp_value =
|
||||
wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request))
|
||||
.await
|
||||
.unwrap_throw();
|
||||
let resp = resp_value.unchecked_into::<web_sys::Response>();
|
||||
let text = wasm_bindgen_futures::JsFuture::from(resp.text().unwrap_throw())
|
||||
.await
|
||||
.unwrap_throw()
|
||||
.as_string()
|
||||
.unwrap_throw();
|
||||
|
||||
T::from_json(&text).expect_throw(
|
||||
"couldn't deserialize loader data from serde-lite intermediate format",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub trait AnySerialize {
|
||||
fn serialize(&self) -> Option<String>;
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
impl<T> AnySerialize for T
|
||||
where
|
||||
T: Any + Serializable + 'static,
|
||||
{
|
||||
fn serialize(&self) -> Option<String> {
|
||||
self.to_json().ok()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Loader {
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
pub(crate) data: Rc<dyn Fn(Scope, ParamsMap, Url) -> PinnedFuture<Box<dyn AnySerialize>>>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
pub fn call_loader(&self, cx: Scope) -> PinnedFuture<Box<dyn AnySerialize>> {
|
||||
let route = use_route(cx);
|
||||
let params = use_params_map(cx).get();
|
||||
let location = use_location(cx);
|
||||
let url = Url {
|
||||
origin: String::default(), // don't care what the origin is for this purpose
|
||||
pathname: route.path().into(), // only use this route path, not all matched routes
|
||||
search: location.search.get(), // reload when any of query string changes
|
||||
hash: String::default(), // hash is only client-side, shouldn't refire
|
||||
};
|
||||
(self.data)(cx, params, url)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, Fu, T> From<F> for Loader
|
||||
where
|
||||
F: Fn(Scope, ParamsMap, Url) -> Fu + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
T: Any + Serializable + 'static,
|
||||
{
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
fn from(f: F) -> Self {
|
||||
let wrapped_fn = move |cx, params, url| {
|
||||
let res = f(cx, params, url);
|
||||
Box::pin(async move { Box::new(res.await) as Box<dyn AnySerialize> })
|
||||
as PinnedFuture<Box<dyn AnySerialize>>
|
||||
};
|
||||
|
||||
Self {
|
||||
data: Rc::new(wrapped_fn),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
fn from(f: F) -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Loader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Loader").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ssr", not(feature = "hydrate")))]
|
||||
pub async fn loader_to_json(view: impl Fn(Scope) -> String + 'static) -> Option<String> {
|
||||
let (data, _, disposer) = run_scope_undisposed(move |cx| async move {
|
||||
let _shell = view(cx);
|
||||
|
||||
let mut route = use_context::<crate::RouteContext>(cx)?;
|
||||
// get the innermost route matched by this path
|
||||
while let Some(child) = route.child() {
|
||||
route = child;
|
||||
}
|
||||
let data = route
|
||||
.loader()
|
||||
.as_ref()
|
||||
.map(|loader| loader.call_loader(cx))?;
|
||||
|
||||
data.await.serialize()
|
||||
});
|
||||
let data = data.await;
|
||||
disposer.dispose();
|
||||
data
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod loader;
|
||||
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
pub use loader::*;
|
||||
|
||||
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
@@ -67,7 +67,6 @@
|
||||
//! element=move |cx| view! { cx, <Contact/> }
|
||||
//! />
|
||||
//! // a fallback if the /:id segment is missing from the URL
|
||||
//! // doesn't need any data, so no loader is provided
|
||||
//! <Route
|
||||
//! path=""
|
||||
//! element=move |_| view! { cx, <p class="contact">"Select a contact."</p> }
|
||||
@@ -85,10 +84,6 @@
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // Loaders are async functions that have access to the reactive scope,
|
||||
//! // map of matched URL params for that route, and the URL
|
||||
//! // They are reloaded whenever the params or URL change
|
||||
//!
|
||||
//! type ContactSummary = (); // TODO!
|
||||
//! type Contact = (); // TODO!()
|
||||
//!
|
||||
@@ -145,14 +140,12 @@
|
||||
#![feature(type_name_of_val)]
|
||||
|
||||
mod components;
|
||||
mod data;
|
||||
mod fetch;
|
||||
mod history;
|
||||
mod hooks;
|
||||
mod matching;
|
||||
|
||||
pub use components::*;
|
||||
pub use data::*;
|
||||
pub use fetch::*;
|
||||
pub use history::*;
|
||||
pub use hooks::*;
|
||||
|
||||
@@ -3,12 +3,9 @@ use std::rc::Rc;
|
||||
use leptos::leptos_dom::Child;
|
||||
use leptos::*;
|
||||
|
||||
use crate::Loader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RouteDefinition {
|
||||
pub path: &'static str,
|
||||
pub loader: Option<Loader>,
|
||||
pub children: Vec<RouteDefinition>,
|
||||
pub element: Rc<dyn Fn(Scope) -> Child>,
|
||||
}
|
||||
@@ -17,7 +14,6 @@ impl std::fmt::Debug for RouteDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RouteDefinition")
|
||||
.field("path", &self.path)
|
||||
.field("loader", &self.loader)
|
||||
.field("children", &self.children)
|
||||
.finish()
|
||||
}
|
||||
@@ -33,7 +29,6 @@ impl Default for RouteDefinition {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path: Default::default(),
|
||||
loader: Default::default(),
|
||||
children: Default::default(),
|
||||
element: Rc::new(|_| Child::Null),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user