mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
290 lines
9.2 KiB
Rust
290 lines
9.2 KiB
Rust
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},
|
|
};
|
|
|
|
/// See [`query_signal`].
|
|
#[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)
|
|
}
|
|
|
|
/// See [`query_signal_with_options`].
|
|
#[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)
|
|
}
|
|
|
|
/// Constructs a signal synchronized with a specific URL query parameter.
|
|
///
|
|
/// The function creates a bidirectional sync mechanism between the state encapsulated in a signal and a URL query parameter.
|
|
/// This means that any change to the state will update the URL, and vice versa, making the function especially useful
|
|
/// for maintaining state consistency across page reloads.
|
|
///
|
|
/// The `key` argument is the unique identifier for the query parameter to be synced with the state.
|
|
/// It is important to note that only one state can be tied to a specific key at any given time.
|
|
///
|
|
/// The function operates with types that can be parsed from and formatted into strings, denoted by `T`.
|
|
/// If the parsing fails for any reason, the function treats the value as `None`.
|
|
/// The URL parameter can be cleared by setting the signal to `None`.
|
|
///
|
|
/// ```rust
|
|
/// use leptos::prelude::*;
|
|
/// use leptos_router::hooks::query_signal;
|
|
///
|
|
/// #[component]
|
|
/// pub fn SimpleQueryCounter() -> impl IntoView {
|
|
/// let (count, set_count) = query_signal::<i32>("count");
|
|
/// let clear = move |_| set_count.set(None);
|
|
/// let decrement =
|
|
/// move |_| set_count.set(Some(count.get().unwrap_or(0) - 1));
|
|
/// let increment =
|
|
/// move |_| set_count.set(Some(count.get().unwrap_or(0) + 1));
|
|
///
|
|
/// view! {
|
|
/// <div>
|
|
/// <button on:click=clear>"Clear"</button>
|
|
/// <button on:click=decrement>"-1"</button>
|
|
/// <span>"Value: " {move || count.get().unwrap_or(0)} "!"</span>
|
|
/// <button on:click=increment>"+1"</button>
|
|
/// </div>
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
#[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())
|
|
}
|
|
|
|
/// Constructs a signal synchronized with a specific URL query parameter.
|
|
///
|
|
/// This is the same as [`query_signal`], but allows you to specify additional navigation options.
|
|
#[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()
|
|
}
|
|
|
|
/*
|
|
/// Returns the current [`RouterContext`], containing information about the router's state.
|
|
#[track_caller]
|
|
pub(crate) fn use_router() -> RouterContext {
|
|
if let Some(router) = use_context::<RouterContext>() {
|
|
router
|
|
} else {
|
|
leptos::leptos_dom::debug_warn!(
|
|
"You must call use_router() within a <Router/> component {:?}",
|
|
std::panic::Location::caller()
|
|
);
|
|
panic!("You must call use_router() within a <Router/> component");
|
|
}
|
|
}
|
|
*/
|
|
|
|
/// Returns the current [`Location`], which contains reactive variables
|
|
#[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>.",
|
|
)
|
|
}
|
|
|
|
/// Returns a raw key-value map of route params.
|
|
#[track_caller]
|
|
pub fn use_params_map() -> Memo<ParamsMap> {
|
|
use_params_raw().into()
|
|
}
|
|
|
|
/// Returns the current route params, parsed into the given type, or an error.
|
|
#[track_caller]
|
|
pub fn use_params<T>() -> Memo<Result<T, ParamsError>>
|
|
where
|
|
T: Params + PartialEq + Send + Sync + 'static,
|
|
{
|
|
// TODO this can be optimized in future to map over the signal, rather than cloning
|
|
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
|
|
})
|
|
}
|
|
|
|
/// Gives reactive access to the current URL.
|
|
#[track_caller]
|
|
pub fn use_url() -> ReadSignal<Url> {
|
|
use_url_raw().read_only().into()
|
|
}
|
|
|
|
/// Returns a raw key-value map of the URL search query.
|
|
#[track_caller]
|
|
pub fn use_query_map() -> Memo<ParamsMap> {
|
|
let url = use_url_raw();
|
|
Memo::new(move |_| url.with(|url| url.search_params().clone()))
|
|
}
|
|
|
|
/// Returns the current URL search query, parsed into the given type, or an error.
|
|
#[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>);
|
|
|
|
/// Resolves the given path relative to the current route.
|
|
#[track_caller]
|
|
pub(crate) fn use_resolved_path(
|
|
path: impl Fn() -> String + Send + Sync + 'static,
|
|
) -> ArcMemo<Option<String>> {
|
|
let router = use_context::<RouterContext>()
|
|
.expect("called use_resolved_path outside a <Router>");
|
|
// TODO make this work with flat routes too?
|
|
let matched = use_context::<Matched>().map(|n| n.0);
|
|
ArcMemo::new(move |_| {
|
|
let path = path();
|
|
if path.starts_with('/') {
|
|
Some(path)
|
|
} else {
|
|
router
|
|
.resolve_path(
|
|
&path,
|
|
matched.as_ref().map(|n| n.get()).as_deref(),
|
|
)
|
|
.map(|n| n.to_string())
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Returns a function that can be used to navigate to a new route.
|
|
///
|
|
/// This should only be called on the client; it does nothing during
|
|
/// server rendering.
|
|
///
|
|
/// ```rust
|
|
/// # if false { // can't actually navigate, no <Router/>
|
|
/// let navigate = leptos_router::hooks::use_navigate();
|
|
/// navigate("/", Default::default());
|
|
/// # }
|
|
/// ```
|
|
#[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)
|
|
}
|
|
|
|
/// Returns a reactive string that contains the route that was matched for
|
|
/// this [`Route`](crate::components::Route).
|
|
#[track_caller]
|
|
pub fn use_matched() -> Memo<String> {
|
|
use_context::<Matched>()
|
|
.expect("use_matched called outside a matched Route")
|
|
.0
|
|
.into()
|
|
}
|