initial async routing work (to support bundle splitting)

This commit is contained in:
Greg Johnston 2024-05-05 15:21:06 -04:00
parent cfba7a2797
commit a7b1152910
11 changed files with 355 additions and 163 deletions

View file

@ -16,6 +16,7 @@ either_of = { workspace = true }
or_poisoned = { workspace = true } or_poisoned = { workspace = true }
reactive_graph = { workspace = true } reactive_graph = { workspace = true }
tachys = { workspace = true, features = ["reactive_graph"] } tachys = { workspace = true, features = ["reactive_graph"] }
futures = "0.3"
url = "2" url = "2"
js-sys = { version = "0.3" } js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" } wasm-bindgen = { version = "0.2" }

View file

@ -79,6 +79,7 @@ where
let location = let location =
BrowserUrl::new().expect("could not access browser navigation"); // TODO options here BrowserUrl::new().expect("could not access browser navigation"); // TODO options here
location.init(base.clone()); location.init(base.clone());
provide_context(location.clone());
location.as_url().clone() location.as_url().clone()
}; };
// provide router context // provide router context
@ -199,6 +200,7 @@ where
FallbackFn: Fn() -> Fallback + Send + 'static, FallbackFn: Fn() -> Fallback + Send + 'static,
Fallback: IntoView + 'static, Fallback: IntoView + 'static,
{ {
let location = use_context::<BrowserUrl>();
let RouterContext { let RouterContext {
current_url, base, .. current_url, base, ..
} = use_context() } = use_context()
@ -220,6 +222,7 @@ where
let outer_owner = let outer_owner =
Owner::current().expect("creating Routes, but no Owner was found"); Owner::current().expect("creating Routes, but no Owner was found");
move || NestedRoutesView { move || NestedRoutesView {
location: location.clone(),
routes: routes.clone(), routes: routes.clone(),
outer_owner: outer_owner.clone(), outer_owner: outer_owner.clone(),
url: current_url.clone(), url: current_url.clone(),
@ -241,8 +244,7 @@ where
FallbackFn: Fn() -> Fallback + Send + 'static, FallbackFn: Fn() -> Fallback + Send + 'static,
Fallback: IntoView + 'static, Fallback: IntoView + 'static,
{ {
use either_of::Either; let location = use_context::<BrowserUrl>();
let RouterContext { let RouterContext {
current_url, base, .. current_url, base, ..
} = use_context() } = use_context()
@ -264,12 +266,16 @@ where
let outer_owner = let outer_owner =
Owner::current().expect("creating Router, but no Owner was found"); Owner::current().expect("creating Router, but no Owner was found");
let params = ArcRwSignal::new(ParamsMap::new()); let params = ArcRwSignal::new(ParamsMap::new());
move || FlatRoutesView { move || {
routes: routes.clone(), path.track();
path: path.clone(), FlatRoutesView {
fallback: fallback(), location: location.clone(),
outer_owner: outer_owner.clone(), routes: routes.clone(),
params: params.clone(), path: path.clone(),
fallback: fallback(),
outer_owner: outer_owner.clone(),
params: params.clone(),
}
} }
} }

View file

@ -1,25 +1,28 @@
use crate::{ use crate::{
location::{Location, RequestUrl, Url}, location::{Location, LocationProvider, RequestUrl, Url},
matching::Routes, matching::Routes,
params::ParamsMap, params::ParamsMap,
resolve_path::resolve_path, resolve_path::resolve_path,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method, ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
PathSegment, RouteList, RouteListing, RouteMatchId, PathSegment, RouteList, RouteListing, RouteMatchId,
}; };
use either_of::Either; use any_spawner::Executor;
use either_of::{Either, EitherFuture, EitherOf3};
use leptos::{component, oco::Oco, IntoView}; use leptos::{component, oco::Oco, IntoView};
use or_poisoned::OrPoisoned; use or_poisoned::OrPoisoned;
use reactive_graph::{ use reactive_graph::{
computed::{ArcMemo, Memo}, computed::{ArcMemo, Memo, ScopedFuture},
owner::{provide_context, use_context, Owner}, owner::{provide_context, use_context, Owner},
signal::{ArcRwSignal, ArcTrigger}, signal::{ArcRwSignal, ArcTrigger},
traits::{Get, Read, ReadUntracked, Set, Track, Trigger}, traits::{Get, GetUntracked, Read, ReadUntracked, Set, Track, Trigger},
}; };
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell,
iter, iter,
marker::PhantomData, marker::PhantomData,
mem, mem,
rc::Rc,
sync::{ sync::{
mpsc::{self, Receiver, Sender}, mpsc::{self, Receiver, Sender},
Arc, Mutex, Arc, Mutex,
@ -37,7 +40,8 @@ use tachys::{
}, },
}; };
pub(crate) struct FlatRoutesView<Defs, Fal, R> { pub(crate) struct FlatRoutesView<Loc, Defs, Fal, R> {
pub location: Option<Loc>,
pub routes: Routes<Defs, R>, pub routes: Routes<Defs, R>,
pub path: ArcMemo<String>, pub path: ArcMemo<String>,
pub fallback: Fal, pub fallback: Fal,
@ -45,13 +49,14 @@ pub(crate) struct FlatRoutesView<Defs, Fal, R> {
pub params: ArcRwSignal<ParamsMap>, pub params: ArcRwSignal<ParamsMap>,
} }
impl<Defs, Fal, R> FlatRoutesView<Defs, Fal, R> impl<Loc, Defs, Fal, R> FlatRoutesView<Loc, Defs, Fal, R>
where where
Loc: LocationProvider,
Defs: MatchNestedRoutes<R>, Defs: MatchNestedRoutes<R>,
Fal: Render<R>, Fal: Render<R>,
R: Renderer + 'static, R: Renderer + 'static,
{ {
pub fn choose( pub async fn choose(
self, self,
) -> Either<Fal, <Defs::Match as MatchInterface<R>>::View> { ) -> Either<Fal, <Defs::Match as MatchInterface<R>>::View> {
let FlatRoutesView { let FlatRoutesView {
@ -60,60 +65,121 @@ where
fallback, fallback,
outer_owner, outer_owner,
params, params,
..
} = self; } = self;
outer_owner.with(|| { outer_owner
provide_context(params.clone().read_only()); .with(|| {
let new_match = routes.match_route(&path.read()); provide_context(params.clone().read_only());
match new_match { let new_match = routes.match_route(&path.read());
None => Either::Left(fallback), match new_match {
Some(matched) => { None => EitherFuture::Left {
let new_params = inner: async move { fallback },
matched.to_params().into_iter().collect::<ParamsMap>(); },
params.set(new_params); Some(matched) => {
let (view, child) = matched.into_view_and_child(); let new_params = matched
.to_params()
.into_iter()
.collect::<ParamsMap>();
params.set(new_params);
let (view, child) = matched.into_view_and_child();
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if child.is_some() { if child.is_some() {
panic!( panic!(
"<FlatRoutes> should not be used with nested \ "<FlatRoutes> should not be used with nested \
routes." routes."
); );
}
EitherFuture::Right {
inner: ScopedFuture::new(view.choose()),
}
} }
let view = view.choose();
Either::Right(view)
} }
} })
}) .await
} }
} }
impl<Defs, Fal, R> Render<R> for FlatRoutesView<Defs, Fal, R> impl<Loc, Defs, Fal, R> Render<R> for FlatRoutesView<Loc, Defs, Fal, R>
where where
Defs: MatchNestedRoutes<R>, Loc: LocationProvider,
Fal: Render<R>, Defs: MatchNestedRoutes<R> + 'static,
Fal: Render<R> + 'static,
R: Renderer + 'static, R: Renderer + 'static,
{ {
type State = <Either<Fal, <Defs::Match as MatchInterface<R>>::View> as Render<R>>::State; type State = Rc<
RefCell<
// TODO loading indicator
<EitherOf3<(), Fal, <Defs::Match as MatchInterface<R>>::View> as Render<
R,
>>::State,
>,
>;
fn build(self) -> Self::State { fn build(self) -> Self::State {
self.choose().build() let state = Rc::new(RefCell::new(EitherOf3::A(()).build()));
let spawned_path = self.path.get_untracked();
let current_path = self.path.clone();
let location = self.location.clone();
let route = self.choose();
Executor::spawn_local({
let state = Rc::clone(&state);
async move {
let loaded_route = route.await;
// only update the route if it's still the current path
// i.e., if we've navigated away before this has loaded, do nothing
if &spawned_path == &*current_path.read_untracked() {
let new_view = match loaded_route {
Either::Left(i) => EitherOf3::B(i),
Either::Right(i) => EitherOf3::C(i),
};
new_view.rebuild(&mut state.borrow_mut());
if let Some(location) = location {
location.ready_to_complete();
}
}
}
});
state
} }
fn rebuild(self, state: &mut Self::State) { fn rebuild(self, state: &mut Self::State) {
self.choose().rebuild(state); let spawned_path = self.path.get_untracked();
let current_path = self.path.clone();
let location = self.location.clone();
let route = self.choose();
Executor::spawn_local({
let state = Rc::clone(&*state);
async move {
let loaded_route = route.await;
// only update the route if it's still the current path
// i.e., if we've navigated away before this has loaded, do nothing
if &spawned_path == &*current_path.read_untracked() {
let new_view = match loaded_route {
Either::Left(i) => EitherOf3::B(i),
Either::Right(i) => EitherOf3::C(i),
};
new_view.rebuild(&mut state.borrow_mut());
if let Some(location) = location {
location.ready_to_complete();
}
}
}
});
} }
} }
impl<Defs, Fal, R> AddAnyAttr<R> for FlatRoutesView<Defs, Fal, R> impl<Loc, Defs, Fal, R> AddAnyAttr<R> for FlatRoutesView<Loc, Defs, Fal, R>
where where
Defs: MatchNestedRoutes<R> + Send, Loc: LocationProvider + Send,
Fal: RenderHtml<R>, Defs: MatchNestedRoutes<R> + Send + 'static,
Fal: RenderHtml<R> + 'static,
R: Renderer + 'static, R: Renderer + 'static,
{ {
type Output<SomeNewAttr: leptos::attr::Attribute<R>> = type Output<SomeNewAttr: leptos::attr::Attribute<R>> =
FlatRoutesView<Defs, Fal, R>; FlatRoutesView<Loc, Defs, Fal, R>;
fn add_any_attr<NewAttr: leptos::attr::Attribute<R>>( fn add_any_attr<NewAttr: leptos::attr::Attribute<R>>(
self, self,
@ -126,10 +192,11 @@ where
} }
} }
impl<Defs, Fal, R> RenderHtml<R> for FlatRoutesView<Defs, Fal, R> impl<Loc, Defs, Fal, R> RenderHtml<R> for FlatRoutesView<Loc, Defs, Fal, R>
where where
Defs: MatchNestedRoutes<R> + Send, Loc: LocationProvider + Send,
Fal: RenderHtml<R>, Defs: MatchNestedRoutes<R> + Send + 'static,
Fal: RenderHtml<R> + 'static,
R: Renderer + 'static, R: Renderer + 'static,
{ {
type AsyncOutput = Self; type AsyncOutput = Self;
@ -190,7 +257,8 @@ where
RouteList::register(RouteList::from(routes)); RouteList::register(RouteList::from(routes));
} else { } else {
self.choose().to_html_with_buf(buf, position); todo!()
// self.choose().to_html_with_buf(buf, position);
} }
} }
@ -201,8 +269,9 @@ where
) where ) where
Self: Sized, Self: Sized,
{ {
self.choose() todo!()
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position); // self.choose()
// .to_html_async_with_buf::<OUT_OF_ORDER>(buf, position);
} }
fn hydrate<const FROM_SERVER: bool>( fn hydrate<const FROM_SERVER: bool>(
@ -210,6 +279,7 @@ where
cursor: &Cursor<R>, cursor: &Cursor<R>,
position: &PositionState, position: &PositionState,
) -> Self::State { ) -> Self::State {
self.choose().hydrate::<FROM_SERVER>(cursor, position) todo!()
// self.choose().hydrate::<FROM_SERVER>(cursor, position)
} }
} }

View file

@ -3,9 +3,18 @@ use super::{
}; };
use crate::{navigate::UseNavigate, params::ParamsMap}; use crate::{navigate::UseNavigate, params::ParamsMap};
use core::fmt; use core::fmt;
use futures::channel::oneshot;
use js_sys::{try_iter, Array, JsString, Reflect}; use js_sys::{try_iter, Array, JsString, Reflect};
use or_poisoned::OrPoisoned;
use reactive_graph::{signal::ArcRwSignal, traits::Set}; use reactive_graph::{signal::ArcRwSignal, traits::Set};
use std::{borrow::Cow, boxed::Box, rc::Rc, string::String}; use std::{
borrow::Cow,
boxed::Box,
cell::RefCell,
rc::Rc,
string::String,
sync::{Arc, Mutex},
};
use tachys::dom::{document, window}; use tachys::dom::{document, window};
use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use web_sys::{Event, HtmlAnchorElement, MouseEvent, UrlSearchParams}; use web_sys::{Event, HtmlAnchorElement, MouseEvent, UrlSearchParams};
@ -13,6 +22,7 @@ use web_sys::{Event, HtmlAnchorElement, MouseEvent, UrlSearchParams};
#[derive(Clone)] #[derive(Clone)]
pub struct BrowserUrl { pub struct BrowserUrl {
url: ArcRwSignal<Url>, url: ArcRwSignal<Url>,
pending_navigation: Arc<Mutex<Option<oneshot::Sender<()>>>>,
} }
impl fmt::Debug for BrowserUrl { impl fmt::Debug for BrowserUrl {
@ -49,7 +59,11 @@ impl LocationProvider for BrowserUrl {
fn new() -> Result<Self, JsValue> { fn new() -> Result<Self, JsValue> {
let url = ArcRwSignal::new(Self::current()?); let url = ArcRwSignal::new(Self::current()?);
Ok(Self { url }) let pending_navigation = Default::default();
Ok(Self {
url,
pending_navigation,
})
} }
fn as_url(&self) -> &ArcRwSignal<Url> { fn as_url(&self) -> &ArcRwSignal<Url> {
@ -94,10 +108,17 @@ impl LocationProvider for BrowserUrl {
let window = window(); let window = window();
let navigate = { let navigate = {
let url = self.url.clone(); let url = self.url.clone();
let pending = Arc::clone(&self.pending_navigation);
move |new_url, loc| { move |new_url, loc| {
let (tx, rx) = oneshot::channel::<()>();
*pending.lock().or_poisoned() = Some(tx);
url.set(new_url); url.set(new_url);
async move { async move {
Self::complete_navigation(&loc); // if it has been canceled, ignore
// otherwise, complete navigation -- i.e., set URL in address bar
if rx.await.is_ok() {
Self::complete_navigation(&loc);
}
} }
} }
}; };
@ -146,6 +167,12 @@ impl LocationProvider for BrowserUrl {
.expect("couldn't add `popstate` listener to `window`"); .expect("couldn't add `popstate` listener to `window`");
} }
fn ready_to_complete(&self) {
if let Some(tx) = self.pending_navigation.lock().or_poisoned().take() {
tx.send(());
}
}
fn complete_navigation(loc: &LocationChange) { fn complete_navigation(loc: &LocationChange) {
let history = window().history().unwrap(); let history = window().history().unwrap();

View file

@ -114,7 +114,7 @@ impl Default for LocationChange {
} }
} }
pub trait LocationProvider: Sized { pub trait LocationProvider: Clone + 'static {
type Error: Debug; type Error: Debug;
fn new() -> Result<Self, Self::Error>; fn new() -> Result<Self, Self::Error>;
@ -126,6 +126,10 @@ pub trait LocationProvider: Sized {
/// Sets up any global event listeners or other initialization needed. /// Sets up any global event listeners or other initialization needed.
fn init(&self, base: Option<Cow<'static, str>>); fn init(&self, base: Option<Cow<'static, str>>);
/// Should be called after a navigation when all route components and data have been loaded and
/// the URL can be updated.
fn ready_to_complete(&self);
/// Update the browser's history to reflect a new location. /// Update the browser's history to reflect a new location.
fn complete_navigation(loc: &LocationChange); fn complete_navigation(loc: &LocationChange);

View file

@ -1,4 +1,5 @@
use either_of::*; use either_of::*;
use std::future::Future;
use tachys::{renderer::Renderer, view::Render}; use tachys::{renderer::Renderer, view::Render};
pub trait ChooseView<R> pub trait ChooseView<R>
@ -6,21 +7,22 @@ where
Self: Send + 'static, Self: Send + 'static,
R: Renderer + 'static, R: Renderer + 'static,
{ {
type Output: Render<R> + Send; type Output;
fn choose(self) -> Self::Output; fn choose(self) -> impl Future<Output = Self::Output>;
} }
impl<F, View, R> ChooseView<R> for F impl<F, ViewFut, R> ChooseView<R> for F
where where
F: Fn() -> View + Send + 'static, F: Fn() -> ViewFut + Send + 'static,
View: Render<R> + Send, ViewFut: Future,
ViewFut::Output: Render<R> + Send,
R: Renderer + 'static, R: Renderer + 'static,
{ {
type Output = View; type Output = ViewFut::Output;
fn choose(self) -> Self::Output { async fn choose(self) -> Self::Output {
self() self().await
} }
} }
@ -30,7 +32,7 @@ where
{ {
type Output = (); type Output = ();
fn choose(self) -> Self::Output {} async fn choose(self) -> Self::Output {}
} }
impl<A, B, Rndr> ChooseView<Rndr> for Either<A, B> impl<A, B, Rndr> ChooseView<Rndr> for Either<A, B>
@ -41,10 +43,10 @@ where
{ {
type Output = Either<A::Output, B::Output>; type Output = Either<A::Output, B::Output>;
fn choose(self) -> Self::Output { async fn choose(self) -> Self::Output {
match self { match self {
Either::Left(f) => Either::Left(f.choose()), Either::Left(f) => Either::Left(f.choose().await),
Either::Right(f) => Either::Right(f.choose()), Either::Right(f) => Either::Right(f.choose().await),
} }
} }
} }
@ -58,9 +60,9 @@ macro_rules! tuples {
{ {
type Output = $either<$($ty::Output,)*>; type Output = $either<$($ty::Output,)*>;
fn choose(self ) -> Self::Output { async fn choose(self ) -> Self::Output {
match self { match self {
$($either::$ty(f) => $either::$ty(f.choose()),)* $($either::$ty(f) => $either::$ty(f.choose().await),)*
} }
} }
} }

View file

@ -4,7 +4,7 @@ use super::{
}; };
use crate::{ChooseView, MatchParams, SsrMode, GeneratedRouteData}; use crate::{ChooseView, MatchParams, SsrMode, GeneratedRouteData};
use core::{fmt, iter}; use core::{fmt, iter};
use std::{borrow::Cow, marker::PhantomData, sync::atomic::{AtomicU16, Ordering}}; use std::{borrow::Cow, marker::PhantomData, sync::atomic::{AtomicU16, Ordering}, future::Future};
use either_of::Either; use either_of::Either;
use tachys::{ use tachys::{
renderer::Renderer, renderer::Renderer,
@ -118,16 +118,17 @@ where
} }
} }
impl<ParamsIter, Child, ViewFn, View, Rndr> MatchInterface<Rndr> impl<ParamsIter, Child, ViewFn, ViewFut, Rndr> MatchInterface<Rndr>
for NestedMatch<ParamsIter, Child, ViewFn> for NestedMatch<ParamsIter, Child, ViewFn>
where where
Rndr: Renderer + 'static, Rndr: Renderer + 'static,
Child: MatchInterface<Rndr> + MatchParams + 'static, Child: MatchInterface<Rndr> + MatchParams + 'static,
ViewFn: Fn() -> View + Send + 'static, ViewFn: Fn() -> ViewFut + Send + 'static,
View: Render<Rndr> + RenderHtml<Rndr> + Send + 'static, ViewFut: Future,
ViewFut::Output: Render<Rndr> + RenderHtml<Rndr> + Send + 'static,
{ {
type Child = Child; type Child = Child;
type View = ViewFn::Output; type View = ViewFut::Output;
fn as_id(&self) -> RouteMatchId { fn as_id(&self) -> RouteMatchId {
self.id self.id
@ -147,7 +148,7 @@ where
} }
} }
impl<Segments, Children, Data, ViewFn, View, Rndr> MatchNestedRoutes<Rndr> impl<Segments, Children, Data, ViewFn, ViewFut, Rndr> MatchNestedRoutes<Rndr>
for NestedRoute<Segments, Children, Data, ViewFn, Rndr> for NestedRoute<Segments, Children, Data, ViewFn, Rndr>
where where
Self: 'static, Self: 'static,
@ -159,11 +160,12 @@ where
Children::Match: MatchParams, Children::Match: MatchParams,
Children: 'static, Children: 'static,
<Children::Match as MatchParams>::Params: Clone, <Children::Match as MatchParams>::Params: Clone,
ViewFn: Fn() -> View + Send + Clone + 'static, ViewFn: Fn() -> ViewFut + Send + Clone + 'static,
View: Render<Rndr> + RenderHtml<Rndr> + Send + 'static, ViewFut: Future,
ViewFut::Output: Render<Rndr> + RenderHtml<Rndr> + Send + 'static,
{ {
type Data = Data; type Data = Data;
type View = View; type View = ViewFut::Output;
type Match = NestedMatch<iter::Chain< type Match = NestedMatch<iter::Chain<
<Segments::ParamsIter as IntoIterator>::IntoIter, <Segments::ParamsIter as IntoIterator>::IntoIter,
Either<iter::Empty::< Either<iter::Empty::<

View file

@ -1,25 +1,30 @@
use crate::{ use crate::{
location::{Location, RequestUrl, Url}, location::{Location, LocationProvider, RequestUrl, Url},
matching::Routes, matching::Routes,
params::ParamsMap, params::ParamsMap,
resolve_path::resolve_path, resolve_path::resolve_path,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method, ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
PathSegment, RouteList, RouteListing, RouteMatchId, PathSegment, RouteList, RouteListing, RouteMatchId,
}; };
use either_of::Either; use any_spawner::Executor;
use either_of::{Either, EitherOf3};
use futures::future::join_all;
use leptos::{component, oco::Oco, IntoView}; use leptos::{component, oco::Oco, IntoView};
use or_poisoned::OrPoisoned; use or_poisoned::OrPoisoned;
use reactive_graph::{ use reactive_graph::{
computed::{ArcMemo, Memo}, computed::{ArcMemo, Memo, ScopedFuture},
owner::{provide_context, use_context, Owner}, owner::{provide_context, use_context, Owner},
signal::{ArcRwSignal, ArcTrigger}, signal::{ArcRwSignal, ArcTrigger},
traits::{Get, Read, ReadUntracked, Set, Track, Trigger}, traits::{Get, Read, ReadUntracked, Set, Track, Trigger},
}; };
use std::{ use std::{
borrow::Cow, cell::RefCell,
future::Future,
iter, iter,
marker::PhantomData, marker::PhantomData,
mem, mem,
pin::Pin,
rc::Rc,
sync::{ sync::{
mpsc::{self, Receiver, Sender}, mpsc::{self, Receiver, Sender},
Arc, Mutex, Arc, Mutex,
@ -32,7 +37,7 @@ use tachys::{
view::{ view::{
add_attr::AddAnyAttr, add_attr::AddAnyAttr,
any_view::{AnyView, AnyViewState, IntoAny}, any_view::{AnyView, AnyViewState, IntoAny},
either::EitherState, either::{EitherOf3State, EitherState},
Mountable, Position, PositionState, Render, RenderHtml, Mountable, Position, PositionState, Render, RenderHtml,
}, },
}; };
@ -41,7 +46,8 @@ pub struct Outlet<R> {
rndr: PhantomData<R>, rndr: PhantomData<R>,
} }
pub(crate) struct NestedRoutesView<Defs, Fal, R> { pub(crate) struct NestedRoutesView<Loc, Defs, Fal, R> {
pub location: Option<Loc>,
pub routes: Routes<Defs, R>, pub routes: Routes<Defs, R>,
pub outer_owner: Owner, pub outer_owner: Owner,
pub url: ArcRwSignal<Url>, pub url: ArcRwSignal<Url>,
@ -62,15 +68,18 @@ where
path: ArcMemo<String>, path: ArcMemo<String>,
search_params: ArcMemo<ParamsMap>, search_params: ArcMemo<ParamsMap>,
outlets: Vec<RouteContext<R>>, outlets: Vec<RouteContext<R>>,
view: EitherState<Fal::State, AnyViewState<R>, R>, // TODO loading fallback
view: Rc<RefCell<EitherOf3State<(), Fal, AnyView<R>, R>>>,
} }
impl<Defs, Fal, R> Render<R> for NestedRoutesView<Defs, Fal, R> impl<Loc, Defs, Fal, R> Render<R> for NestedRoutesView<Loc, Defs, Fal, R>
where where
Loc: LocationProvider,
Defs: MatchNestedRoutes<R>, Defs: MatchNestedRoutes<R>,
Fal: Render<R>, Fal: Render<R> + 'static,
R: Renderer + 'static, R: Renderer + 'static,
{ {
// TODO support fallback while loading
type State = NestedRouteViewState<Fal, R>; type State = NestedRouteViewState<Fal, R>;
fn build(self) -> Self::State { fn build(self) -> Self::State {
@ -85,20 +94,41 @@ where
.. ..
} = self; } = self;
let mut loaders = Vec::new();
let mut outlets = Vec::new(); let mut outlets = Vec::new();
let new_match = routes.match_route(&path.read()); let new_match = routes.match_route(&path.read());
let view = match new_match {
None => Either::Left(fallback), // start with an empty view because we'll be loading routes async
let view = EitherOf3::A(()).build();
let view = Rc::new(RefCell::new(view));
let matched_view = match new_match {
None => EitherOf3::B(fallback),
Some(route) => { Some(route) => {
route.build_nested_route(base, &mut outlets, &outer_owner); route.build_nested_route(
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
outer_owner.with(|| { outer_owner.with(|| {
Either::Right( EitherOf3::C(
Outlet(OutletProps::builder().build()).into_any(), Outlet(OutletProps::builder().build()).into_any(),
) )
}) })
} }
} };
.build();
Executor::spawn_local({
let view = Rc::clone(&view);
let loaders = mem::take(&mut loaders);
async move {
let triggers = join_all(loaders).await;
for trigger in triggers {
trigger.trigger();
}
matched_view.rebuild(&mut *view.borrow_mut());
}
});
NestedRouteViewState { NestedRouteViewState {
outlets, outlets,
@ -115,25 +145,40 @@ where
match new_match { match new_match {
None => { None => {
Either::<Fal, AnyView<R>>::Left(self.fallback) EitherOf3::<(), Fal, AnyView<R>>::B(self.fallback)
.rebuild(&mut state.view); .rebuild(&mut state.view.borrow_mut());
state.outlets.clear(); state.outlets.clear();
} }
Some(route) => { Some(route) => {
let mut loaders = Vec::new();
route.rebuild_nested_route( route.rebuild_nested_route(
self.base, self.base,
&mut 0, &mut 0,
&mut loaders,
&mut state.outlets, &mut state.outlets,
&self.outer_owner, &self.outer_owner,
); );
// hmm...
let location = self.location.clone();
Executor::spawn_local(async move {
let triggers = join_all(loaders).await;
// tell each one of the outlet triggers that it's ready
for trigger in triggers {
trigger.trigger();
}
if let Some(loc) = location {
loc.ready_to_complete();
}
});
// if it was on the fallback, show the view instead // if it was on the fallback, show the view instead
if matches!(state.view.state, Either::Left(_)) { if matches!(state.view.borrow().state, EitherOf3::B(_)) {
self.outer_owner.with(|| { self.outer_owner.with(|| {
Either::<Fal, AnyView<R>>::Right( EitherOf3::<(), Fal, AnyView<R>>::C(
Outlet(OutletProps::builder().build()).into_any(), Outlet(OutletProps::builder().build()).into_any(),
) )
.rebuild(&mut state.view); .rebuild(&mut *state.view.borrow_mut());
}) })
} }
} }
@ -141,14 +186,15 @@ where
} }
} }
impl<Defs, Fal, R> AddAnyAttr<R> for NestedRoutesView<Defs, Fal, R> impl<Loc, Defs, Fal, R> AddAnyAttr<R> for NestedRoutesView<Loc, Defs, Fal, R>
where where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes<R> + Send, Defs: MatchNestedRoutes<R> + Send,
Fal: RenderHtml<R>, Fal: RenderHtml<R> + 'static,
R: Renderer + 'static, R: Renderer + 'static,
{ {
type Output<SomeNewAttr: leptos::attr::Attribute<R>> = type Output<SomeNewAttr: leptos::attr::Attribute<R>> =
NestedRoutesView<Defs, Fal, R>; NestedRoutesView<Loc, Defs, Fal, R>;
fn add_any_attr<NewAttr: leptos::attr::Attribute<R>>( fn add_any_attr<NewAttr: leptos::attr::Attribute<R>>(
self, self,
@ -161,10 +207,11 @@ where
} }
} }
impl<Defs, Fal, R> RenderHtml<R> for NestedRoutesView<Defs, Fal, R> impl<Loc, Defs, Fal, R> RenderHtml<R> for NestedRoutesView<Loc, Defs, Fal, R>
where where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes<R> + Send, Defs: MatchNestedRoutes<R> + Send,
Fal: RenderHtml<R>, Fal: RenderHtml<R> + 'static,
R: Renderer + 'static, R: Renderer + 'static,
{ {
type AsyncOutput = Self; type AsyncOutput = Self;
@ -238,7 +285,13 @@ where
let view = match new_match { let view = match new_match {
None => Either::Left(fallback), None => Either::Left(fallback),
Some(route) => { Some(route) => {
route.build_nested_route(base, &mut outlets, &outer_owner); route.build_nested_route(
base,
// TODO loaders here
&mut Vec::new(),
&mut outlets,
&outer_owner,
);
outer_owner.with(|| { outer_owner.with(|| {
Either::Right( Either::Right(
Outlet(OutletProps::builder().build()).into_any(), Outlet(OutletProps::builder().build()).into_any(),
@ -273,7 +326,13 @@ where
let view = match new_match { let view = match new_match {
None => Either::Left(fallback), None => Either::Left(fallback),
Some(route) => { Some(route) => {
route.build_nested_route(base, &mut outlets, &outer_owner); route.build_nested_route(
base,
// TODO loaders
&mut Vec::new(),
&mut outlets,
&outer_owner,
);
outer_owner.with(|| { outer_owner.with(|| {
Either::Right( Either::Right(
Outlet(OutletProps::builder().build()).into_any(), Outlet(OutletProps::builder().build()).into_any(),
@ -302,18 +361,26 @@ where
let mut outlets = Vec::new(); let mut outlets = Vec::new();
let new_match = routes.match_route(&path.read()); let new_match = routes.match_route(&path.read());
let view = match new_match { let view = Rc::new(RefCell::new(
None => Either::Left(fallback), match new_match {
Some(route) => { None => EitherOf3::B(fallback),
route.build_nested_route(base, &mut outlets, &outer_owner); Some(route) => {
outer_owner.with(|| { route.build_nested_route(
Either::Right( base,
Outlet(OutletProps::builder().build()).into_any(), // TODO loaders in hydration
) &mut Vec::new(),
}) &mut outlets,
&outer_owner,
);
outer_owner.with(|| {
EitherOf3::C(
Outlet(OutletProps::builder().build()).into_any(),
)
})
}
} }
} .hydrate::<FROM_SERVER>(cursor, position),
.hydrate::<FROM_SERVER>(cursor, position); ));
NestedRouteViewState { NestedRouteViewState {
outlets, outlets,
@ -378,6 +445,7 @@ where
fn build_nested_route( fn build_nested_route(
self, self,
base: Option<Oco<'static, str>>, base: Option<Oco<'static, str>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext<R>>, outlets: &mut Vec<RouteContext<R>>,
parent: &Owner, parent: &Owner,
); );
@ -386,6 +454,7 @@ where
self, self,
base: Option<Oco<'static, str>>, base: Option<Oco<'static, str>>,
items: &mut usize, items: &mut usize,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext<R>>, outlets: &mut Vec<RouteContext<R>>,
parent: &Owner, parent: &Owner,
); );
@ -399,6 +468,7 @@ where
fn build_nested_route( fn build_nested_route(
self, self,
base: Option<Oco<'static, str>>, base: Option<Oco<'static, str>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext<R>>, outlets: &mut Vec<RouteContext<R>>,
parent: &Owner, parent: &Owner,
) { ) {
@ -428,7 +498,7 @@ where
// add this outlet to the end of the outlet stack used for diffing // add this outlet to the end of the outlet stack used for diffing
let outlet = RouteContext { let outlet = RouteContext {
id: self.as_id(), id: self.as_id(),
trigger, trigger: trigger.clone(),
params, params,
owner: owner.clone(), owner: owner.clone(),
matched: ArcRwSignal::new(self.as_matched().to_string()), matched: ArcRwSignal::new(self.as_matched().to_string()),
@ -441,9 +511,14 @@ where
// send the initial view through the channel, and recurse through the children // send the initial view through the channel, and recurse through the children
let (view, child) = self.into_view_and_child(); let (view, child) = self.into_view_and_child();
tx.send(Box::new({ loaders.push(Box::pin({
let owner = outlet.owner.clone(); let owner = outlet.owner.clone();
move || owner.with(|| view.choose().into_any()) async move {
let view =
owner.with(|| ScopedFuture::new(view.choose())).await;
tx.send(Box::new(move || owner.with(|| view.into_any())));
trigger
}
})); }));
// and share the outlet with the parent via context // and share the outlet with the parent via context
@ -455,7 +530,7 @@ where
// this is important because to build the view, we need access to the outlet // this is important because to build the view, we need access to the outlet
// and the outlet will be returned from building this child // and the outlet will be returned from building this child
if let Some(child) = child { if let Some(child) = child {
child.build_nested_route(base, outlets, &owner); child.build_nested_route(base, loaders, outlets, &owner);
} }
} }
@ -463,6 +538,7 @@ where
self, self,
base: Option<Oco<'static, str>>, base: Option<Oco<'static, str>>,
items: &mut usize, items: &mut usize,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext<R>>, outlets: &mut Vec<RouteContext<R>>,
parent: &Owner, parent: &Owner,
) { ) {
@ -470,7 +546,7 @@ where
match current { match current {
// if there's nothing currently in the routes at this point, build from here // if there's nothing currently in the routes at this point, build from here
None => { None => {
self.build_nested_route(base, outlets, parent); self.build_nested_route(base, loaders, outlets, parent);
} }
Some(current) => { Some(current) => {
// a unique ID for each route, which allows us to compare when we get new matches // a unique ID for each route, which allows us to compare when we get new matches
@ -514,13 +590,21 @@ where
// send the new view, with the new owner, through the channel to the Outlet, // send the new view, with the new owner, through the channel to the Outlet,
// and notify the trigger so that the reactive view inside the Outlet tracking // and notify the trigger so that the reactive view inside the Outlet tracking
// the trigger runs again // the trigger runs again
current.tx.send({ loaders.push(Box::pin({
let owner = owner.clone(); let owner = owner.clone();
Box::new(move || { let trigger = current.trigger.clone();
owner.with(|| view.choose().into_any()) let tx = current.tx.clone();
}) async move {
}); let view = owner
current.trigger.trigger(); .with(|| ScopedFuture::new(view.choose()))
.await;
tx.send(Box::new(move || {
owner.with(|| view.into_any())
}));
drop(old_owner);
trigger
}
}));
// remove all the items lower in the tree // remove all the items lower in the tree
// if this match is different, all its children will also be different // if this match is different, all its children will also be different
@ -531,6 +615,7 @@ where
let mut new_outlets = Vec::new(); let mut new_outlets = Vec::new();
child.build_nested_route( child.build_nested_route(
base, base,
loaders,
&mut new_outlets, &mut new_outlets,
&owner, &owner,
); );
@ -545,7 +630,9 @@ where
if let Some(child) = child { if let Some(child) = child {
let owner = current.owner.clone(); let owner = current.owner.clone();
*items += 1; *items += 1;
child.rebuild_nested_route(base, items, outlets, &owner); child.rebuild_nested_route(
base, items, loaders, outlets, &owner,
);
} }
} }
} }
@ -597,7 +684,6 @@ where
); );
move || { move || {
trigger.track(); trigger.track();
rx.try_recv().map(|view| view())
rx.try_recv().map(|view| view()).unwrap()
} }
} }

View file

@ -305,31 +305,3 @@ where
self.write().insert_before_this(parent, child) self.write().insert_before_this(parent, child)
} }
} }
impl<Rndr, Fal, Output> Mountable<Rndr>
for Rc<RefCell<EitherState<Fal, Output, Rndr>>>
where
Fal: Mountable<Rndr>,
Output: Mountable<Rndr>,
Rndr: Renderer,
{
fn unmount(&mut self) {
self.borrow_mut().unmount();
}
fn mount(
&mut self,
parent: &<Rndr as Renderer>::Element,
marker: Option<&<Rndr as Renderer>::Node>,
) {
self.borrow_mut().mount(parent, marker);
}
fn insert_before_this(
&self,
parent: &<Rndr as Renderer>::Element,
child: &mut dyn Mountable<Rndr>,
) -> bool {
self.borrow_mut().insert_before_this(parent, child)
}
}

View file

@ -425,8 +425,8 @@ macro_rules! tuples {
$($ty: Render<Rndr>,)* $($ty: Render<Rndr>,)*
Rndr: Renderer Rndr: Renderer
{ {
state: [<EitherOf $num>]<$($ty::State,)*>, pub state: [<EitherOf $num>]<$($ty::State,)*>,
marker: Rndr::Placeholder, pub marker: Rndr::Placeholder,
} }
impl<$($ty,)* Rndr> Mountable<Rndr> for [<EitherOf $num State>]<$($ty,)* Rndr> impl<$($ty,)* Rndr> Mountable<Rndr> for [<EitherOf $num State>]<$($ty,)* Rndr>

View file

@ -1,7 +1,7 @@
use self::add_attr::AddAnyAttr; use self::add_attr::AddAnyAttr;
use crate::{hydration::Cursor, renderer::Renderer, ssr::StreamBuilder}; use crate::{hydration::Cursor, renderer::Renderer, ssr::StreamBuilder};
use parking_lot::RwLock; use parking_lot::RwLock;
use std::{future::Future, sync::Arc}; use std::{cell::RefCell, future::Future, rc::Rc, sync::Arc};
pub mod add_attr; pub mod add_attr;
pub mod any_view; pub mod any_view;
@ -256,6 +256,28 @@ where
} }
} }
impl<T, R> Mountable<R> for Rc<RefCell<T>>
where
T: Mountable<R>,
R: Renderer,
{
fn unmount(&mut self) {
self.borrow_mut().unmount()
}
fn mount(&mut self, parent: &R::Element, marker: Option<&R::Node>) {
self.borrow_mut().mount(parent, marker);
}
fn insert_before_this(
&self,
parent: &<R as Renderer>::Element,
child: &mut dyn Mountable<R>,
) -> bool {
self.borrow().insert_before_this(parent, child)
}
}
/// Allows data to be added to a static template. /// Allows data to be added to a static template.
pub trait ToTemplate { pub trait ToTemplate {
const TEMPLATE: &'static str = ""; const TEMPLATE: &'static str = "";