use crate::{
location::Location,
matching::Params,
route::MatchedRoute,
router::{FallbackOrView, Router},
static_render::StaticDataMap,
PathSegment, RouteList, RouteListing, SsrMode,
};
use reactive_graph::{
memo::Memo,
signal::ArcRwSignal,
traits::{SignalGet, SignalSet, SignalWith, Track},
untrack, Owner,
};
use std::{marker::PhantomData, mem};
use tachydom::{
hydration::Cursor,
log,
renderer::Renderer,
ssr::StreamBuilder,
view::{Mountable, Position, PositionState, Render, RenderHtml},
};
#[allow(non_snake_case)]
pub fn ReactiveRouter<Rndr, Loc, DefFn, Defs, FallbackFn, Fallback>(
mut location: Loc,
routes: DefFn,
fallback: FallbackFn,
) -> impl RenderHtml
where
DefFn: Fn() -> Defs + 'static,
Defs: 'static,
Loc: Location + Clone + 'static,
Rndr: Renderer + 'static,
Rndr::Element: Clone,
Rndr::Node: Clone,
FallbackFn: Fn() -> Fallback + Clone + 'static,
Fallback: Render + 'static,
Router<Rndr, Loc, Defs, FallbackFn>: FallbackOrView,
<Router<Rndr, Loc, Defs, FallbackFn> as FallbackOrView>::Output: RenderHtml,
{
let url = ArcRwSignal::new(location.try_to_url().unwrap_or_default());
location.set_navigation_hook({
let url = url.clone();
move |new_url| {
tachydom::log(&format!("setting url to {new_url:?}"));
url.set(new_url)
}
});
location.init();
let owner = Owner::current().unwrap();
move || {
url.track();
ReactiveRouterInner {
owner: owner.clone(),
inner: Router::new(location.clone(), routes(), fallback.clone()),
fal: PhantomData,
}
}
}
struct ReactiveRouterInner<Rndr, Loc, Defs, FallbackFn, Fallback>
where
Rndr: Renderer,
{
owner: Owner,
inner: Router<Rndr, Loc, Defs, FallbackFn>,
fal: PhantomData<Fallback>,
}
impl<Rndr, Loc, Defs, FallbackFn, Fallback> Render
for ReactiveRouterInner<Rndr, Loc, Defs, FallbackFn, Fallback>
where
Loc: Location,
Rndr: Renderer,
Rndr::Element: Clone,
Rndr::Node: Clone,
FallbackFn: Fn() -> Fallback,
Fallback: Render,
Router<Rndr, Loc, Defs, FallbackFn>: FallbackOrView,
<Router<Rndr, Loc, Defs, FallbackFn> as FallbackOrView>::Output: Render,
{
type State =
ReactiveRouterInnerState<Rndr, Loc, Defs, FallbackFn, Fallback>;
fn build(self) -> Self::State {
let (prev_id, inner) = self.inner.fallback_or_view();
let owner = self.owner.with(Owner::new);
ReactiveRouterInnerState {
inner: owner.with(|| inner.build()),
owner,
prev_id,
fal: PhantomData,
}
}
fn rebuild(self, state: &mut Self::State) {
let (new_id, view) = self.inner.fallback_or_view();
if new_id != state.prev_id {
state.owner = self.owner.with(Owner::new)
};
state.owner.with(|| view.rebuild(&mut state.inner));
}
}
impl<Rndr, Loc, Defs, FallbackFn, Fallback> RenderHtml
for ReactiveRouterInner<Rndr, Loc, Defs, FallbackFn, Fallback>
where
Loc: Location,
Rndr: Renderer,
Rndr::Element: Clone,
Rndr::Node: Clone,
FallbackFn: Fn() -> Fallback,
Fallback: Render,
Router<Rndr, Loc, Defs, FallbackFn>: FallbackOrView,
<Router<Rndr, Loc, Defs, FallbackFn> as FallbackOrView>::Output: RenderHtml,
{
const MIN_LENGTH: usize = <<Router<Rndr, Loc, Defs, FallbackFn> as FallbackOrView>::Output as RenderHtml>::MIN_LENGTH;
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 mut routes = RouteList::new();
self.inner.generate_route_list(&mut routes);
routes.push(RouteListing::from_path([PathSegment::Static(
"".into(),
)]));
RouteList::register(routes);
} else {
let (id, view) = self.inner.fallback_or_view();
view.to_html_with_buf(buf, position, escape)
}
}
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,
{
self.inner
.fallback_or_view()
.1
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape)
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
let (prev_id, inner) = self.inner.fallback_or_view();
let owner = self.owner.with(Owner::new);
ReactiveRouterInnerState {
inner: owner
.with(|| inner.hydrate::<FROM_SERVER>(cursor, position)),
owner,
prev_id,
fal: PhantomData,
}
}
}
struct ReactiveRouterInnerState<Rndr, Loc, Defs, FallbackFn, Fallback>
where
Router<Rndr, Loc, Defs, FallbackFn>: FallbackOrView,
<Router<Rndr, Loc, Defs, FallbackFn> as FallbackOrView>::Output: Render,
Rndr: Renderer,
{
owner: Owner,
prev_id: &'static str,
inner: <<Router<Rndr, Loc, Defs, FallbackFn> as FallbackOrView>::Output as Render>::State,
fal: PhantomData<Fallback>,
}
impl<Rndr, Loc, Defs, FallbackFn, Fallback> Mountable
for ReactiveRouterInnerState<Rndr, Loc, Defs, FallbackFn, Fallback>
where
Router<Rndr, Loc, Defs, FallbackFn>: FallbackOrView,
<Router<Rndr, Loc, Defs, FallbackFn> as FallbackOrView>::Output: Render,
Rndr: Renderer,
{
fn unmount(&mut self) {
self.inner.unmount();
}
fn mount(
&mut self,
parent: &leptos::tachys::renderer::types::Element,
marker: Option<&leptos::tachys::renderer::types::Node>,
) {
self.inner.mount(parent, marker);
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
self.inner.insert_before_this(child)
}
}
pub struct ReactiveMatchedRoute {
pub(crate) search_params: ArcRwSignal<Params<String>>,
pub(crate) params: ArcRwSignal<Params<&'static str>>,
pub(crate) matched: ArcRwSignal<String>,
}
impl ReactiveMatchedRoute {
pub fn param(&self, key: &str) -> Memo<Option<String>> {
let params = self.params.clone();
let key = key.to_owned();
Memo::new(move |_| {
params.with(|p| {
p.iter().find(|n| n.0 == key).map(|(_, v)| v.to_string())
})
})
}
pub fn search(&self, key: &str) -> Memo<Option<String>> {
let params = self.search_params.clone();
let key = key.to_owned();
Memo::new(move |_| {
params.with(|p| {
p.iter().find(|n| n.0 == key).map(|(_, v)| v.to_string())
})
})
}
}
pub fn reactive_route<ViewFn, View>(
view_fn: ViewFn,
) -> impl Fn(MatchedRoute) -> ReactiveRoute<ViewFn, View>
where
ViewFn: Fn(&ReactiveMatchedRoute) -> View + Clone,
View: Render,
Rndr: Renderer,
{
move |matched| ReactiveRoute {
view_fn: view_fn.clone(),
matched,
ty: PhantomData,
}
}
pub struct ReactiveRoute<ViewFn, View>
where
ViewFn: Fn(&ReactiveMatchedRoute) -> View,
View: Render,
Rndr: Renderer,
{
view_fn: ViewFn,
matched: MatchedRoute,
ty: PhantomData,
}
impl<ViewFn, View> Render for ReactiveRoute<ViewFn, View>
where
ViewFn: Fn(&ReactiveMatchedRoute) -> View,
View: Render,
Rndr: Renderer,
{
type State = ReactiveRouteState<View::State>;
fn build(self) -> Self::State {
let MatchedRoute {
search_params,
params,
matched,
} = self.matched;
let matched = ReactiveMatchedRoute {
search_params: ArcRwSignal::new(search_params),
params: ArcRwSignal::new(params),
matched: ArcRwSignal::new(matched),
};
let view_state = untrack(|| (self.view_fn)(&matched).build());
ReactiveRouteState {
matched,
view_state,
}
}
fn rebuild(mut self, state: &mut Self::State) {
let ReactiveRouteState { matched, .. } = state;
matched
.search_params
.set(mem::take(&mut self.matched.search_params));
matched.params.set(mem::take(&mut self.matched.params));
matched.matched.set(mem::take(&mut self.matched.matched));
}
}
impl<ViewFn, View> RenderHtml for ReactiveRoute<ViewFn, View>
where
ViewFn: Fn(&ReactiveMatchedRoute) -> View,
View: RenderHtml,
Rndr: Renderer,
Rndr::Node: Clone,
Rndr::Element: Clone,
{
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool, extra_attrs: Vec<AnyAttribute>) {
let MatchedRoute {
search_params,
params,
matched,
} = self.matched;
let matched = ReactiveMatchedRoute {
search_params: ArcRwSignal::new(search_params),
params: ArcRwSignal::new(params),
matched: ArcRwSignal::new(matched),
};
untrack(|| {
(self.view_fn)(&matched).to_html_with_buf(buf, position, escape)
});
}
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 MatchedRoute {
search_params,
params,
matched,
} = self.matched;
let matched = ReactiveMatchedRoute {
search_params: ArcRwSignal::new(search_params),
params: ArcRwSignal::new(params),
matched: ArcRwSignal::new(matched),
};
untrack(|| {
(self.view_fn)(&matched)
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape)
});
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
let MatchedRoute {
search_params,
params,
matched,
} = self.matched;
let matched = ReactiveMatchedRoute {
search_params: ArcRwSignal::new(search_params),
params: ArcRwSignal::new(params),
matched: ArcRwSignal::new(matched),
};
let view_state = untrack(|| {
(self.view_fn)(&matched).hydrate::<FROM_SERVER>(cursor, position)
});
ReactiveRouteState {
matched,
view_state,
}
}
}
pub struct ReactiveRouteState<State> {
view_state: State,
matched: ReactiveMatchedRoute,
}
impl<State> Drop for ReactiveRouteState<State> {
fn drop(&mut self) {
log("dropping ReactiveRouteState");
}
}
impl<T> Mountable for ReactiveRouteState<T>
where
T: Mountable,
{
fn unmount(&mut self) {
self.view_state.unmount();
}
fn mount(
&mut self,
parent: &<R as Renderer>::Element,
marker: Option<&<R as Renderer>::Node>,
) {
self.view_state.mount(parent, marker);
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
self.view_state.insert_before_this(child)
}
}