use crate::{
components::RouterContext,
location::{Location, Url},
navigate::NavigateOptions,
params::{Params, ParamsError, ParamsMap},
};
use leptos::{leptos_dom::helpers::request_animation_frame, oco::Oco};
use reactive_graph::{
computed::{ArcMemo, Memo},
owner::{expect_context, use_context},
signal::{ArcRwSignal, ReadSignal},
traits::{Get, GetUntracked, ReadUntracked, With, WriteValue},
wrappers::write::SignalSetter,
};
use std::{
str::FromStr,
sync::atomic::{AtomicBool, Ordering},
};
#[track_caller]
#[deprecated = "This has been renamed to `query_signal` to match Rust naming \
conventions."]
pub fn create_query_signal<T>(
key: impl Into<Oco<'static, str>>,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq + Send + Sync,
{
query_signal(key)
}
#[track_caller]
#[deprecated = "This has been renamed to `query_signal_with_options` to mtch \
Rust naming conventions."]
pub fn create_query_signal_with_options<T>(
key: impl Into<Oco<'static, str>>,
nav_options: NavigateOptions,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq + Send + Sync,
{
query_signal_with_options(key, nav_options)
}
#[track_caller]
pub fn query_signal<T>(
key: impl Into<Oco<'static, str>>,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq + Send + Sync,
{
query_signal_with_options::<T>(key, NavigateOptions::default())
}
#[track_caller]
pub fn query_signal_with_options<T>(
key: impl Into<Oco<'static, str>>,
nav_options: NavigateOptions,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq + Send + Sync,
{
static IS_NAVIGATING: AtomicBool = AtomicBool::new(false);
let mut key: Oco<'static, str> = key.into();
let query_map = use_query_map();
let navigate = use_navigate();
let location = use_location();
let RouterContext {
query_mutations, ..
} = expect_context();
let get = Memo::new({
let key = key.clone_inplace();
move |_| {
query_map.with(|map| {
map.get_str(&key).and_then(|value| value.parse().ok())
})
}
});
let set = SignalSetter::map(move |value: Option<T>| {
let path = location.pathname.get_untracked();
let hash = location.hash.get_untracked();
let qs = location.query.read_untracked().to_query_string();
let new_url = format!("{path}{qs}{hash}");
query_mutations
.write_value()
.push((key.clone(), value.as_ref().map(ToString::to_string)));
if !IS_NAVIGATING.load(Ordering::Relaxed) {
IS_NAVIGATING.store(true, Ordering::Relaxed);
request_animation_frame({
let navigate = navigate.clone();
let nav_options = nav_options.clone();
move || {
navigate(&new_url, nav_options.clone());
IS_NAVIGATING.store(false, Ordering::Relaxed)
}
})
}
});
(get, set)
}
#[track_caller]
pub(crate) fn has_router() -> bool {
use_context::<RouterContext>().is_some()
}
#[track_caller]
pub fn use_location() -> Location {
let RouterContext { location, .. } =
use_context().expect("Tried to access Location outside a <Router>.");
location
}
pub(crate) type RawParamsMap = ArcMemo<ParamsMap>;
#[track_caller]
fn use_params_raw() -> RawParamsMap {
use_context().expect(
"Tried to access params outside the context of a matched <Route>.",
)
}
#[track_caller]
pub fn use_params_map() -> Memo<ParamsMap> {
use_params_raw().into()
}
#[track_caller]
pub fn use_params<T>() -> Memo<Result<T, ParamsError>>
where
T: Params + PartialEq + Send + Sync + 'static,
{
let params = use_params_raw();
Memo::new(move |_| params.with(T::from_map))
}
#[track_caller]
fn use_url_raw() -> ArcRwSignal<Url> {
use_context().unwrap_or_else(|| {
let RouterContext { current_url, .. } = use_context().expect(
"Tried to access reactive URL outside a <Router> component.",
);
current_url
})
}
#[track_caller]
pub fn use_url() -> ReadSignal<Url> {
use_url_raw().read_only().into()
}
#[track_caller]
pub fn use_query_map() -> Memo<ParamsMap> {
let url = use_url_raw();
Memo::new(move |_| url.with(|url| url.search_params().clone()))
}
#[track_caller]
pub fn use_query<T>() -> Memo<Result<T, ParamsError>>
where
T: Params + PartialEq + Send + Sync + 'static,
{
let url = use_url_raw();
Memo::new(move |_| url.with(|url| T::from_map(url.search_params())))
}
#[derive(Debug, Clone)]
pub(crate) struct Matched(pub ArcMemo<String>);
#[track_caller]
pub(crate) fn use_resolved_path(
path: impl Fn() -> String + Send + Sync + 'static,
) -> ArcMemo<String> {
let router = use_context::<RouterContext>()
.expect("called use_resolved_path outside a <Router>");
let matched = use_context::<Matched>().map(|n| n.0);
ArcMemo::new(move |_| {
let path = path();
if path.starts_with('/') {
path
} else {
router
.resolve_path(
&path,
matched.as_ref().map(|n| n.get()).as_deref(),
)
.to_string()
}
})
}
#[track_caller]
pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone {
let cx = use_context::<RouterContext>()
.expect("You cannot call `use_navigate` outside a <Router>.");
move |path: &str, options: NavigateOptions| cx.navigate(path, options)
}
#[track_caller]
pub fn use_matched() -> Memo<String> {
use_context::<Matched>()
.expect("use_matched called outside a matched Route")
.0
.into()
}