use crate::{
hooks::Matched,
location::{LocationProvider, Url},
matching::{MatchParams, RouteDefs},
params::ParamsMap,
view_transition::start_view_transition,
ChooseView, MatchInterface, MatchNestedRoutes, PathSegment, RouteList,
RouteListing, RouteMatchId,
};
use any_spawner::Executor;
use either_of::Either;
use futures::FutureExt;
use leptos::attr::{any_attribute::AnyAttribute, Attribute};
use reactive_graph::{
computed::{ArcMemo, ScopedFuture},
owner::{provide_context, Owner},
signal::ArcRwSignal,
traits::{GetUntracked, ReadUntracked, Set},
transition::AsyncTransition,
wrappers::write::SignalSetter,
};
use std::{cell::RefCell, iter, mem, rc::Rc};
use tachys::{
hydration::Cursor,
reactive_graph::OwnedView,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
any_view::{AnyView, AnyViewState, IntoAny},
MarkBranch, Mountable, Position, PositionState, Render, RenderHtml,
},
};
pub(crate) struct FlatRoutesView<Loc, Defs, FalFn> {
pub current_url: ArcRwSignal<Url>,
pub location: Option<Loc>,
pub routes: RouteDefs<Defs>,
pub fallback: FalFn,
pub outer_owner: Owner,
pub set_is_routing: Option<SignalSetter<bool>>,
pub transition: bool,
}
pub(crate) struct FlatRoutesViewState {
#[allow(clippy::type_complexity)]
view: AnyViewState,
id: Option<RouteMatchId>,
owner: Owner,
params: ArcRwSignal<ParamsMap>,
path: String,
url: ArcRwSignal<Url>,
matched: ArcRwSignal<String>,
}
impl Mountable for FlatRoutesViewState {
fn unmount(&mut self) {
self.view.unmount();
}
fn mount(
&mut self,
parent: &leptos::tachys::renderer::types::Element,
marker: Option<&leptos::tachys::renderer::types::Node>,
) {
self.view.mount(parent, marker);
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
self.view.insert_before_this(child)
}
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
self.view.elements()
}
}
impl<Loc, Defs, FalFn, Fal> Render for FlatRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider,
Defs: MatchNestedRoutes + 'static,
FalFn: FnOnce() -> Fal + Send,
Fal: IntoAny,
{
type State = Rc<RefCell<FlatRoutesViewState>>;
fn build(self) -> Self::State {
let FlatRoutesView {
current_url,
routes,
fallback,
outer_owner,
..
} = self;
let current_url = current_url.read_untracked();
let new_match = routes.match_route(current_url.path());
let id = new_match.as_ref().map(|n| n.as_id());
let matched = ArcRwSignal::new(
new_match
.as_ref()
.map(|n| n.as_matched().to_owned())
.unwrap_or_default(),
);
let owner = outer_owner.child();
let url = ArcRwSignal::new(current_url.to_owned());
let path = current_url.path().to_string();
let params = ArcRwSignal::new(
new_match
.as_ref()
.map(|n| n.to_params().into_iter().collect())
.unwrap_or_default(),
);
let params_memo = ArcMemo::from(params.clone());
drop(current_url);
match new_match {
None => Rc::new(RefCell::new(FlatRoutesViewState {
view: fallback().into_any().build(),
id,
owner,
params,
path,
url,
matched,
})),
Some(new_match) => {
let (view, child) = new_match.into_view_and_child();
#[cfg(debug_assertions)]
if child.is_some() {
panic!(
"<FlatRoutes> should not be used with nested routes."
);
}
let mut view = Box::pin(owner.with(|| {
provide_context(params_memo);
provide_context(url.clone());
provide_context(Matched(ArcMemo::from(matched.clone())));
ScopedFuture::new(async move {
OwnedView::new(view.choose().await)
})
}));
match view.as_mut().now_or_never() {
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
view: view.into_any().build(),
id,
owner,
params,
path,
url,
matched,
})),
None => {
let state =
Rc::new(RefCell::new(FlatRoutesViewState {
view: ().into_any().build(),
id,
owner,
params,
path,
url,
matched,
}));
Executor::spawn_local({
let state = Rc::clone(&state);
async move {
let view = view.await;
view.into_any()
.rebuild(&mut state.borrow_mut().view);
}
});
state
}
}
}
}
}
fn rebuild(self, state: &mut Self::State) {
let FlatRoutesView {
current_url,
location,
routes,
fallback,
outer_owner,
set_is_routing,
transition,
} = self;
let url_snapshot = current_url.read_untracked();
let mut initial_state = state.borrow_mut();
if url_snapshot.path() == initial_state.path {
initial_state.url.set(url_snapshot.to_owned());
if let Some(location) = location {
location.ready_to_complete();
}
return;
}
initial_state.path.clear();
initial_state.path.push_str(url_snapshot.path());
let new_match = routes.match_route(url_snapshot.path());
let new_id = new_match.as_ref().map(|n| n.as_id());
let matched_string = new_match
.as_ref()
.map(|n| n.as_matched().to_owned())
.unwrap_or_default();
let matched_params = new_match
.as_ref()
.map(|n| n.to_params().into_iter().collect())
.unwrap_or_default();
if new_id == initial_state.id {
initial_state.params.set(matched_params);
initial_state.matched.set(matched_string);
if let Some(location) = location {
location.ready_to_complete();
}
return;
}
initial_state.id = new_id;
let owner = outer_owner.child();
let url = ArcRwSignal::new(url_snapshot.to_owned());
let params = ArcRwSignal::new(matched_params);
let params_memo = ArcMemo::from(params.clone());
let old_owner = mem::replace(&mut initial_state.owner, owner.clone());
let old_url = mem::replace(&mut initial_state.url, url.clone());
let old_params =
mem::replace(&mut initial_state.params, params.clone());
let new_matched = ArcRwSignal::new(matched_string);
let old_matched =
mem::replace(&mut initial_state.matched, new_matched.clone());
drop(initial_state);
match new_match {
None => {
owner.with(|| {
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(new_matched)));
fallback().into_any().rebuild(&mut state.borrow_mut().view)
});
if let Some(location) = location {
location.ready_to_complete();
}
}
Some(new_match) => {
let (view, child) = new_match.into_view_and_child();
#[cfg(debug_assertions)]
if child.is_some() {
panic!(
"<FlatRoutes> should not be used with nested routes."
);
}
let spawned_path = url_snapshot.path().to_string();
let is_back = location
.as_ref()
.map(|nav| nav.is_back().get_untracked())
.unwrap_or(false);
Executor::spawn_local(owner.with(|| {
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(new_matched)));
ScopedFuture::new({
let state = Rc::clone(state);
async move {
let view = OwnedView::new(
if let Some(set_is_routing) = set_is_routing {
set_is_routing.set(true);
let value =
AsyncTransition::run(|| view.choose())
.await;
set_is_routing.set(false);
value
} else {
view.choose().await
},
);
if current_url.read_untracked().path()
== spawned_path
{
let rebuild = move || {
view.into_any()
.rebuild(&mut state.borrow_mut().view);
};
if transition {
start_view_transition(0, is_back, rebuild);
} else {
rebuild();
}
}
if let Some(location) = location {
location.ready_to_complete();
}
drop(old_owner);
drop(old_params);
drop(old_url);
drop(old_matched);
}
})
}));
}
}
}
}
impl<Loc, Defs, FalFn, Fal> AddAnyAttr for FlatRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send + 'static,
Fal: RenderHtml + 'static,
{
type Output<SomeNewAttr: leptos::attr::Attribute> =
FlatRoutesView<Loc, Defs, FalFn>;
fn add_any_attr<NewAttr: leptos::attr::Attribute>(
self,
_attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
todo!()
}
}
#[derive(Debug)]
pub(crate) struct MatchedRoute(pub String, pub AnyView);
impl MatchedRoute {
fn branch_name(&self) -> String {
format!("{:?}", self.1.as_type_id())
}
}
impl Render for MatchedRoute {
type State = <AnyView as Render>::State;
fn build(self) -> Self::State {
self.1.build()
}
fn rebuild(self, state: &mut Self::State) {
self.1.rebuild(state);
}
}
impl AddAnyAttr for MatchedRoute {
type Output<SomeNewAttr: Attribute> = Self;
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
let MatchedRoute(id, view) = self;
MatchedRoute(id, view.add_any_attr(attr).into_any())
}
}
impl RenderHtml for MatchedRoute {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
self.1.dry_resolve();
}
async fn resolve(self) -> Self::AsyncOutput {
let MatchedRoute(id, view) = self;
let view = view.resolve().await;
MatchedRoute(id, view)
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
let branch_name = (mark_branches && escape).then(|| self.branch_name());
if let Some(bn) = &branch_name {
buf.open_branch(bn);
}
self.1.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
if let Some(bn) = &branch_name {
buf.close_branch(bn);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
let branch_name = (mark_branches && escape).then(|| self.branch_name());
if let Some(bn) = &branch_name {
buf.open_branch(bn);
}
self.1.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
if let Some(bn) = &branch_name {
buf.close_branch(bn);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
self.1.hydrate::<FROM_SERVER>(cursor, position)
}
async fn hydrate_async(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
self.1.hydrate_async(cursor, position).await
}
fn into_owned(self) -> Self::Owned {
self
}
}
impl<Loc, Defs, FalFn, Fal> FlatRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send,
Fal: RenderHtml + 'static,
{
fn choose_ssr(self) -> OwnedView<AnyView> {
let current_url = self.current_url.read_untracked();
let new_match = self.routes.match_route(current_url.path());
let owner = self.outer_owner.child();
let url = ArcRwSignal::new(current_url.to_owned());
let params = ArcRwSignal::new(
new_match
.as_ref()
.map(|n| n.to_params().into_iter().collect::<ParamsMap>())
.unwrap_or_default(),
);
let matched = ArcRwSignal::new(
new_match
.as_ref()
.map(|n| n.as_matched().to_owned())
.unwrap_or_default(),
);
let params_memo = ArcMemo::from(params.clone());
drop(current_url);
let view = match new_match {
None => (self.fallback)().into_any(),
Some(new_match) => {
let id = new_match.as_matched().to_string();
let (view, _) = new_match.into_view_and_child();
let view = owner
.with(|| {
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(matched)));
ScopedFuture::new(async move { view.choose().await })
})
.now_or_never()
.expect("async route used in SSR");
let view = MatchedRoute(id, view);
view.into_any()
}
};
OwnedView::new_with_owner(view, owner)
}
}
impl<Loc, Defs, FalFn, Fal> RenderHtml for FlatRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send + 'static,
Fal: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = <Either<Fal, AnyView> as RenderHtml>::MIN_LENGTH;
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
if RouteList::is_generating() {
let (base, routes) = self.routes.generate_routes();
let routes = routes
.into_iter()
.map(|data| {
let path = base
.into_iter()
.flat_map(|base| {
iter::once(PathSegment::Static(
base.to_string().into(),
))
})
.chain(data.segments)
.collect::<Vec<_>>();
RouteListing::new(
path,
data.ssr_mode,
data.methods,
data.regenerate,
)
})
.collect::<Vec<_>>();
RouteList::register(RouteList::from(routes));
} else {
let view = self.choose_ssr();
view.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
let view = self.choose_ssr();
view.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
extra_attrs,
)
}
#[track_caller]
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
let FlatRoutesView {
current_url,
routes,
fallback,
outer_owner,
..
} = self;
let current_url = current_url.read_untracked();
let new_match = routes.match_route(current_url.path());
let id = new_match.as_ref().map(|n| n.as_id());
let matched = ArcRwSignal::new(
new_match
.as_ref()
.map(|n| n.as_matched().to_owned())
.unwrap_or_default(),
);
let owner = outer_owner.child();
let url = ArcRwSignal::new(current_url.to_owned());
let path = current_url.path().to_string();
let params = ArcRwSignal::new(
new_match
.as_ref()
.map(|n| n.to_params().into_iter().collect())
.unwrap_or_default(),
);
let params_memo = ArcMemo::from(params.clone());
drop(current_url);
match new_match {
None => Rc::new(RefCell::new(FlatRoutesViewState {
view: fallback()
.into_any()
.hydrate::<FROM_SERVER>(cursor, position),
id,
owner,
params,
path,
url,
matched,
})),
Some(new_match) => {
let (view, child) = new_match.into_view_and_child();
#[cfg(debug_assertions)]
if child.is_some() {
panic!(
"<FlatRoutes> should not be used with nested routes."
);
}
let mut view = Box::pin(owner.with(|| {
provide_context(params_memo);
provide_context(url.clone());
provide_context(Matched(ArcMemo::from(matched.clone())));
ScopedFuture::new(async move {
OwnedView::new(view.choose().await)
})
}));
match view.as_mut().now_or_never() {
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
view: view
.into_any()
.hydrate::<FROM_SERVER>(cursor, position),
id,
owner,
params,
path,
url,
matched,
})),
None => {
panic!(
"lazy routes should not be used with \
hydrate_body(); use hydrate_lazy() instead"
);
}
}
}
}
}
async fn hydrate_async(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
let FlatRoutesView {
current_url,
routes,
fallback,
outer_owner,
..
} = self;
let current_url = current_url.read_untracked();
let new_match = routes.match_route(current_url.path());
let id = new_match.as_ref().map(|n| n.as_id());
let matched = ArcRwSignal::new(
new_match
.as_ref()
.map(|n| n.as_matched().to_owned())
.unwrap_or_default(),
);
let owner = outer_owner.child();
let url = ArcRwSignal::new(current_url.to_owned());
let path = current_url.path().to_string();
let params = ArcRwSignal::new(
new_match
.as_ref()
.map(|n| n.to_params().into_iter().collect())
.unwrap_or_default(),
);
let params_memo = ArcMemo::from(params.clone());
drop(current_url);
match new_match {
None => Rc::new(RefCell::new(FlatRoutesViewState {
view: fallback()
.into_any()
.hydrate_async(cursor, position)
.await,
id,
owner,
params,
path,
url,
matched,
})),
Some(new_match) => {
let (view, child) = new_match.into_view_and_child();
#[cfg(debug_assertions)]
if child.is_some() {
panic!(
"<FlatRoutes> should not be used with nested routes."
);
}
let view = Box::pin(owner.with(|| {
provide_context(params_memo);
provide_context(url.clone());
provide_context(Matched(ArcMemo::from(matched.clone())));
ScopedFuture::new(async move {
OwnedView::new(view.choose().await)
})
}));
let view = view.await;
Rc::new(RefCell::new(FlatRoutesViewState {
view: view.into_any().hydrate_async(cursor, position).await,
id,
owner,
params,
path,
url,
matched,
}))
}
}
}
fn into_owned(self) -> Self::Owned {
self
}
}