leptos_router 0.8.13

Router for the Leptos web framework.
Documentation
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<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('/') {
            path
        } else {
            router
                .resolve_path(
                    &path,
                    matched.as_ref().map(|n| n.get()).as_deref(),
                )
                .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()
}