use crate::{
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
ParamsMap, RouterContext, SsrMode,
};
use leptos::{leptos_dom::Transparent, *};
use std::{cell::Cell, rc::Rc};
thread_local! {
static ROUTE_ID: Cell<usize> = Cell::new(0);
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum Method {
#[default]
Get,
Post,
Put,
Delete,
Patch,
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
)]
#[component(transparent)]
pub fn Route<E, F, P>(
cx: Scope,
path: P,
view: F,
#[prop(optional)]
ssr: SsrMode,
#[prop(default = &[Method::Get])]
methods: &'static [Method],
#[prop(optional)]
children: Option<Children>,
) -> impl IntoView
where
E: IntoView,
F: Fn(Scope) -> E + 'static,
P: std::fmt::Display,
{
define_route(
cx,
children,
path.to_string(),
Rc::new(move |cx| view(cx).into_view(cx)),
ssr,
methods,
)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
)]
#[component(transparent)]
pub fn ProtectedRoute<P, E, F, C>(
cx: Scope,
path: P,
redirect_path: P,
condition: C,
view: F,
#[prop(optional)]
ssr: SsrMode,
#[prop(default = &[Method::Get])]
methods: &'static [Method],
#[prop(optional)]
children: Option<Children>,
) -> impl IntoView
where
E: IntoView,
F: Fn(Scope) -> E + 'static,
P: std::fmt::Display + 'static,
C: Fn(Scope) -> bool + 'static,
{
use crate::Redirect;
let redirect_path = redirect_path.to_string();
define_route(
cx,
children,
path.to_string(),
Rc::new(move |cx| {
if condition(cx) {
view(cx).into_view(cx)
} else {
view! { cx, <Redirect path=redirect_path.clone()/> }
.into_view(cx)
}
}),
ssr,
methods,
)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
)]
pub(crate) fn define_route(
cx: Scope,
children: Option<Children>,
path: String,
view: Rc<dyn Fn(Scope) -> View>,
ssr_mode: SsrMode,
methods: &'static [Method],
) -> RouteDefinition {
let children = children
.map(|children| {
children(cx)
.as_children()
.iter()
.filter_map(|child| {
child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>())
})
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
let id = ROUTE_ID.with(|id| {
let next = id.get() + 1;
id.set(next);
next
});
RouteDefinition {
id,
path,
children,
view,
ssr_mode,
methods,
}
}
impl IntoView for RouteDefinition {
fn into_view(self, cx: Scope) -> View {
Transparent::new(self).into_view(cx)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RouteContext {
inner: Rc<RouteContextInner>,
}
impl RouteContext {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
)]
pub(crate) fn new(
cx: Scope,
router: &RouterContext,
child: impl Fn(Scope) -> Option<RouteContext> + 'static,
matcher: impl Fn() -> Option<RouteMatch> + 'static,
) -> Option<Self> {
let base = router.base();
let base = base.path();
let RouteMatch { path_match, route } = matcher()?;
let PathMatch { path, .. } = path_match;
let RouteDefinition {
view: element, id, ..
} = route.key;
let params = create_memo(cx, move |_| {
matcher()
.map(|matched| matched.path_match.params)
.unwrap_or_default()
});
Some(Self {
inner: Rc::new(RouteContextInner {
cx,
id,
base_path: base,
child: Box::new(child),
path: create_rw_signal(cx, path),
original_path: route.original_path.to_string(),
params,
outlet: Box::new(move |cx| Some(element(cx))),
}),
})
}
pub fn cx(&self) -> Scope {
self.inner.cx
}
pub(crate) fn id(&self) -> usize {
self.inner.id
}
pub fn path(&self) -> String {
self.inner.path.get_untracked()
}
pub(crate) fn set_path(&self, path: String) {
self.inner.path.set(path);
}
pub fn original_path(&self) -> &str {
&self.inner.original_path
}
pub fn params(&self) -> Memo<ParamsMap> {
self.inner.params
}
pub(crate) fn base(
cx: Scope,
path: &str,
fallback: Option<fn(Scope) -> View>,
) -> Self {
Self {
inner: Rc::new(RouteContextInner {
cx,
id: 0,
base_path: path.to_string(),
child: Box::new(|_| None),
path: create_rw_signal(cx, path.to_string()),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
outlet: Box::new(move |cx| {
fallback.as_ref().map(move |f| f(cx))
}),
}),
}
}
pub fn resolve_path(&self, to: &str) -> Option<String> {
resolve_path(
&self.inner.base_path,
to,
Some(&self.inner.path.get_untracked()),
)
.map(String::from)
}
pub(crate) fn resolve_path_tracked(&self, to: &str) -> Option<String> {
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.get()))
.map(String::from)
}
pub fn child(&self, cx: Scope) -> Option<RouteContext> {
(self.inner.child)(cx)
}
pub fn outlet(&self, cx: Scope) -> impl IntoView {
(self.inner.outlet)(cx)
}
}
pub(crate) struct RouteContextInner {
cx: Scope,
base_path: String,
pub(crate) id: usize,
pub(crate) child: Box<dyn Fn(Scope) -> Option<RouteContext>>,
pub(crate) path: RwSignal<String>,
pub(crate) original_path: String,
pub(crate) params: Memo<ParamsMap>,
pub(crate) outlet: Box<dyn Fn(Scope) -> Option<View>>,
}
impl PartialEq for RouteContextInner {
fn eq(&self, other: &Self) -> bool {
self.cx == other.cx
&& self.base_path == other.base_path
&& self.path == other.path
&& self.original_path == other.original_path
&& self.params == other.params
}
}
impl std::fmt::Debug for RouteContextInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RouteContextInner")
.field("path", &self.path)
.field("ParamsMap", &self.params)
.finish()
}
}