add more hooks and primitives to router

This commit is contained in:
Greg Johnston 2024-04-21 21:40:50 -04:00
parent 12f2cec5c7
commit 2dd2bb5958
11 changed files with 665 additions and 177 deletions

View file

@ -93,7 +93,9 @@ pub async fn get_contact(id: Option<usize>) -> Option<Contact> {
}
}
fn delay(duration: Duration) -> impl Future<Output = Result<(), Canceled>> {
fn delay(
duration: Duration,
) -> impl Future<Output = Result<(), Canceled>> + Send {
let (tx, rx) = oneshot::channel();
set_timeout(
move || {

View file

@ -4,18 +4,24 @@ use leptos::{
component,
prelude::*,
reactive_graph::{
computed::AsyncDerived,
effect::Effect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
},
view, IntoView,
suspend,
tachys::either::Either,
view, IntoView, Params, Suspense, Transition,
};
use log::{debug, info};
use routing::{
components::{ParentRoute, Route, Router},
components::{ParentRoute, Route, Router, Routes},
hooks::{use_location, use_params},
Outlet,
};
use routing::{
location::{BrowserUrl, Location},
params::Params,
MatchNestedRoutes, NestedRoute, ParamSegment, StaticSegment,
};
@ -29,45 +35,43 @@ pub fn RouterExample() -> impl IntoView {
// contexts are passed down through the route tree
provide_context(ExampleContext(0));
/*let router = Router::new(
Routes::new((
NestedRoute::new(StaticSegment("contacts"), ContactList).child((
NestedRoute::new(StaticSegment(""), |_| "Select a contact."),
// TODO: fix it so empty param doesn't match here, if we reverse the order of
// these two
NestedRoute::new(ParamSegment("id"), Contact),
)),
//NestedRoute::new(StaticSegment(""), ContactList),
NestedRoute::new(StaticSegment("settings"), Settings),
NestedRoute::new(StaticSegment("about"), About),
)),
|| "This page could not be found.",
);*/
view! {
<nav>
// ordinary <a> elements can be used for client-side navigation
// using <A> has two effects:
// 1) ensuring that relative routing works properly for nested routes
// 2) setting the `aria-current` attribute on the current link,
// for a11y and styling purposes
<Router>
<nav>
// ordinary <a> elements can be used for client-side navigation
// using <A> has two effects:
// 1) ensuring that relative routing works properly for nested routes
// 2) setting the `aria-current` attribute on the current link,
// for a11y and styling purposes
<a href="/contacts">"Contacts"</a>
<a href="/about">"About"</a>
<a href="/settings">"Settings"</a>
<a href="/redirect-home">"Redirect to Home"</a>
</nav>
<Router fallback=|| "This page could not be found.">
<ParentRoute path=StaticSegment("contacts") view=ContactList>
<Route path=StaticSegment("") view=|| "Select a contact."/>
<Route path=ParamSegment(":id") view=Contact/>
</ParentRoute>
<Route path=StaticSegment("settings") view=Settings/>
<Route path=StaticSegment("about") view=About/>
<a href="/contacts">"Contacts"</a>
<a href="/about">"About"</a>
<a href="/settings">"Settings"</a>
<a href="/redirect-home">"Redirect to Home"</a>
</nav>
<main>
<Routes fallback=|| "This page could not be found.">
<ContactRoutes/>
<Route path=StaticSegment("settings") view=Settings/>
<Route path=StaticSegment("about") view=About/>
</Routes>
</main>
</Router>
}
}
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component]
pub fn ContactRoutes() -> impl MatchNestedRoutes<Dom> + Clone {
view! {
<ParentRoute path=StaticSegment("contacts") view=ContactList>
<Route path=StaticSegment("") view=|| "Select a contact."/>
<Route path=ParamSegment("id") view=Contact/>
</ParentRoute>
}
}
#[component]
pub fn ContactList() -> impl IntoView {
info!("rendering <ContactList/>");
@ -79,59 +83,39 @@ pub fn ContactList() -> impl IntoView {
info!("cleaning up <ContactList/>");
});
let location = use_location();
let contacts =
AsyncDerived::new(move || get_contacts(location.search.get()));
let contacts = suspend!(
// this data doesn't change frequently so we can use .map().collect() instead of a keyed <For/>
contacts.await
.into_iter()
.map(|contact| {
// TODO <A>
view! {
<li><a href=contact.id.to_string()><span>{contact.first_name} " " {contact.last_name}</span></a></li>
}
})
.collect::<Vec<_>>()
);
view! {
<div class="contact-list">
<h1>"Contacts"</h1>
<li>
<a href="/contacts/1">1</a>
</li>
<li>
<a href="/contacts/2">2</a>
</li>
<li>
<a href="/contacts/3">3</a>
</li>
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
<ul>{contacts}</ul>
</Suspense>
<Outlet/>
</div>
}
/*let location = use_location();
let contacts = create_resource(move || location.search.get(), get_contacts);
let contacts = move || {
contacts.get().map(|contacts| {
// this data doesn't change frequently so we can use .map().collect() instead of a keyed <For/>
contacts
.into_iter()
.map(|contact| {
view! {
<li><A href=contact.id.to_string()><span>{&contact.first_name} " " {&contact.last_name}</span></A></li>
}
})
.collect_view()
})
};
view! {
<div class="contact-list">
<h1>"Contacts"</h1>
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
{move || view! { <ul>{contacts}</ul>}}
</Suspense>
<AnimatedOutlet
class="outlet"
outro="fadeOut"
intro="fadeIn"
/>
</div>
}*/
}
/*#[derive(Params, PartialEq, Clone, Debug)]
#[derive(Params, PartialEq, Clone, Debug)]
pub struct ContactParams {
// Params isn't implemented for usize, only Option<usize>
id: Option<usize>,
}*/
}
#[component]
pub fn Contact() -> impl IntoView {
info!("rendering <Contact/>");
@ -144,51 +128,27 @@ pub fn Contact() -> impl IntoView {
info!("cleaning up <Contact/>");
});
view! {
<div class="contact">
<h2>"Contact"</h2>
// {move || format!("{:#?}", params.get())}
</div>
}
let params = use_params::<ContactParams>();
//let params = use_params::<ContactParams>();
/*let contact = create_resource(
move || {
let contact = AsyncDerived::new(move || {
get_contact(
params
.get()
.map(|params| params.id.unwrap_or_default())
.ok()
},
// any of the following would work (they're identical)
// move |id| async move { get_contact(id).await }
// move |id| get_contact(id),
// get_contact
get_contact,
);
Effect::new(move |_| {
info!("params = {:#?}", params.get());
.ok(),
)
});
let contact_display = move || match contact.get() {
// None => loading, but will be caught by Suspense fallback
// I'm only doing this explicitly for the example
None => None,
// Some(None) => has loaded and found no contact
Some(None) => Some(
view! { <p>"No contact with this ID was found."</p> }.into_any(),
),
// Some(Some) => has loaded and found a contact
Some(Some(contact)) => Some(
view! {
<section class="card">
<h1>{contact.first_name} " " {contact.last_name}</h1>
<p>{contact.address_1} <br/> {contact.address_2}</p>
</section>
}
.into_any(),
),
};
let contact_display = suspend!(match contact.await {
None =>
Either::Left(view! { <p>"No contact with this ID was found."</p> }),
Some(contact) => Either::Right(view! {
<section class="card">
<h1>{contact.first_name} " " {contact.last_name}</h1>
<p>{contact.address_1} <br/> {contact.address_2}</p>
</section>
}),
});
view! {
<div class="contact">
@ -196,7 +156,7 @@ pub fn Contact() -> impl IntoView {
view! { <p>"Loading..."</p> }
}>{contact_display}</Transition>
</div>
}*/
}
}
#[component]

View file

@ -19,8 +19,8 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let span = field.span();
quote_spanned! {
span=> #ident: <#ty as ::leptos_router::IntoParam>::into_param(
map.get(#field_name_string).map(::std::string::String::as_str),
span=> #ident: <#ty as ::routing::params::IntoParam>::into_param(
map.get(#field_name_string),
#field_name_string
)?
}
@ -32,7 +32,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let gen = quote! {
impl Params for #name {
fn from_map(map: &::leptos_router::ParamsMap) -> Result<Self, ::leptos_router::ParamsError> {
fn from_map(map: &::routing::params::ParamsMap) -> Result<Self, ::routing::params::ParamsError> {
Ok(Self {
#(#fields,)*
})

View file

@ -14,9 +14,10 @@ url = "2"
js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" }
tracing = { version = "0.1", optional = true }
paste = "1.0.14"
once_cell = "1.19.0"
send_wrapper = "0.6.0"
paste = "1"
once_cell = "1"
send_wrapper = "0.6"
thiserror = "1"
[dependencies.web-sys]
version = "0.3"
@ -48,4 +49,4 @@ features = [
[features]
tracing = ["dep:tracing"]
nightly = []

View file

@ -1,11 +1,19 @@
use crate::{
location::{BrowserUrl, Location},
location::{BrowserUrl, Location, LocationProvider, State, Url},
MatchNestedRoutes, NestedRoute, NestedRoutesView, Routes,
};
use leptos::{children::ToChildren, component, IntoView};
use reactive_graph::{computed::ArcMemo, owner::Owner, traits::Read};
use leptos::{
children::{ToChildren, TypedChildren},
component, IntoView,
};
use reactive_graph::{
computed::ArcMemo,
owner::{provide_context, use_context, Owner},
signal::{ArcRwSignal, RwSignal},
traits::Read,
};
use std::{borrow::Cow, marker::PhantomData};
use tachys::renderer::dom::Dom;
use tachys::renderer::{dom::Dom, Renderer};
#[derive(Debug)]
pub struct RouteChildren<Children>(Children);
@ -24,6 +32,50 @@ where
RouteChildren(f())
}
}
#[component]
pub fn Router<Chil>(
/// The base URL for the router. Defaults to `""`.
#[prop(optional, into)]
base: Option<Cow<'static, str>>,
// TODO these prop
///// A fallback that should be shown if no route is matched.
//#[prop(optional)]
//fallback: Option<fn() -> View>,
///// A signal that will be set while the navigation process is underway.
//#[prop(optional, into)]
//set_is_routing: Option<SignalSetter<bool>>,
///// How trailing slashes should be handled in [`Route`] paths.
//#[prop(optional)]
//trailing_slash: TrailingSlash,
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [`Routes`](crate::Routes) component somewhere
/// to define and display [`Route`](crate::Route)s.
children: TypedChildren<Chil>,
/// A unique identifier for this router, allowing you to mount multiple Leptos apps with
/// different routes from the same server.
#[prop(optional)]
id: usize,
) -> impl IntoView
where
Chil: IntoView,
{
let location =
BrowserUrl::new().expect("could not access browser navigation"); // TODO options here
location.init(base.clone());
let url = location.as_url().clone();
provide_context(url.clone());
provide_context(Location::new(
url.read_only().into(),
// TODO state
RwSignal::new(State::new(None)).read_only(),
));
let children = children.into_inner();
children()
}
/*
#[component]
pub fn FlatRouter<Children, FallbackFn, Fallback>(
@ -43,7 +95,7 @@ where
}*/
#[component]
pub fn Router<Defs, FallbackFn, Fallback>(
pub fn Routes<Defs, FallbackFn, Fallback>(
#[prop(optional, into)] base: Option<Cow<'static, str>>,
fallback: FallbackFn,
children: RouteChildren<Defs>,
@ -53,11 +105,9 @@ where
FallbackFn: Fn() -> Fallback + Send + 'static,
Fallback: IntoView + 'static,
{
let url = use_context::<ArcRwSignal<Url>>()
.expect("<Routes> should be used inside a <Router> component");
let routes = Routes::new(children.into_inner());
let location =
BrowserUrl::new().expect("could not access browser navigation"); // TODO options here
location.init(base.clone());
let url = location.as_url().clone();
let path = ArcMemo::new({
let url = url.clone();
move |_| url.read().path().to_string()

266
routing/src/hooks.rs Normal file
View file

@ -0,0 +1,266 @@
use crate::{
location::{Location, Url},
params::{Params, ParamsError, ParamsMap},
};
use leptos::oco::Oco;
use reactive_graph::{
computed::Memo,
owner::use_context,
signal::{ArcReadSignal, ArcRwSignal, ReadSignal},
traits::{Get, With},
};
use std::{rc::Rc, str::FromStr};
/*
/// 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::*;
/// use leptos_router::*;
///
/// #[component]
/// pub fn SimpleQueryCounter() -> impl IntoView {
/// let (count, set_count) = create_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 create_query_signal<T>(
key: impl Into<Oco<'static, str>>,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq,
{
create_query_signal_with_options::<T>(key, NavigateOptions::default())
}
#[track_caller]
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,
{
let mut key: Oco<'static, str> = key.into();
let query_map = use_query_map();
let navigate = use_navigate();
let location = use_location();
let get = create_memo({
let key = key.clone_inplace();
move |_| {
query_map
.with(|map| map.get(&key).and_then(|value| value.parse().ok()))
}
});
let set = SignalSetter::map(move |value: Option<T>| {
let mut new_query_map = query_map.get();
match value {
Some(value) => {
new_query_map.insert(key.to_string(), value.to_string());
}
None => {
new_query_map.remove(&key);
}
}
let qs = new_query_map.to_query_string();
let path = location.pathname.get_untracked();
let hash = location.hash.get_untracked();
let new_url = format!("{path}{qs}{hash}");
navigate(&new_url, nav_options.clone());
});
(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 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 {
use_context().expect("Tried to access Location outside a <Router>.")
}
#[track_caller]
fn use_params_raw() -> ArcReadSignal<ParamsMap> {
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> {
// TODO this can be optimized in future to map over the signal, rather than cloning
let params = use_params_raw();
Memo::new(move |_| params.get())
}
/// 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,
{
// 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()
.expect("Tried to access reactive URL outside a <Router> component.")
}
#[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,
{
let url = use_url_raw();
Memo::new(move |_| url.with(|url| T::from_map(url.search_params())))
}
/*
/// Resolves the given path relative to the current route.
#[track_caller]
pub fn use_resolved_path(
path: impl Fn() -> String + 'static,
) -> Memo<Option<String>> {
let route = use_route();
create_memo(move |_| {
let path = path();
if path.starts_with('/') {
Some(path)
} else {
route.resolve_path_tracked(&path)
}
})
}*/
/*
/// 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
/// # use leptos::{request_animation_frame, create_runtime};
/// # let runtime = create_runtime();
/// # if false { // can't actually navigate, no <Router/>
/// let navigate = leptos_router::use_navigate();
/// navigate("/", Default::default());
/// # }
/// # runtime.dispose();
/// ```
#[track_caller]
pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone {
let router = use_router();
move |to, options| {
let router = Rc::clone(&router.inner);
let to = to.to_string();
if cfg!(any(feature = "csr", feature = "hydrate")) {
request_animation_frame(move || {
#[allow(unused_variables)]
if let Err(e) = router.navigate_from_route(&to, &options) {
leptos::logging::debug_warn!("use_navigate error: {e:?}");
}
});
} else {
leptos::logging::warn!(
"The navigation function returned by `use_navigate` should \
not be called during server rendering."
);
}
}
}*/
/*
/// Returns a signal that tells you whether you are currently navigating backwards.
pub(crate) fn use_is_back_navigation() -> ReadSignal<bool> {
let router = use_router();
router.inner.is_back.read_only()
}
/// Resolves a redirect location to an (absolute) URL.
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
let origin = match window().location().origin() {
Ok(origin) => origin,
Err(e) => {
leptos::logging::error!("Failed to get origin: {:#?}", e);
return None;
}
};
// TODO: Use server function's URL as base instead.
let base = origin;
match web_sys::Url::new_with_base(loc, &base) {
Ok(url) => Some(url),
Err(e) => {
leptos::logging::error!(
"Invalid redirect location: {}",
e.as_string().unwrap_or_default(),
);
None
}
}
}
*/

View file

@ -1,10 +1,15 @@
#![forbid(unsafe_code)]
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
pub mod components;
mod generate_route_list;
pub mod hooks;
pub mod location;
mod matching;
mod method;
mod nested_router;
mod params;
pub mod params;
//mod router;
mod ssr_mode;
mod static_route;
@ -13,7 +18,6 @@ pub use generate_route_list::*;
pub use matching::*;
pub use method::*;
pub use nested_router::*;
pub use params::*;
//pub use router::*;
pub use ssr_mode::*;
pub use static_route::*;

View file

@ -1,5 +1,7 @@
use super::{handle_anchor_click, Location, LocationChange, State, Url, BASE};
use crate::params::Params;
use super::{
handle_anchor_click, LocationChange, LocationProvider, State, Url, BASE,
};
use crate::params::ParamsMap;
use core::fmt;
use js_sys::{try_iter, Array, JsString, Reflect};
use reactive_graph::{signal::ArcRwSignal, traits::Set};
@ -42,7 +44,7 @@ impl BrowserUrl {
}
}
impl Location for BrowserUrl {
impl LocationProvider for BrowserUrl {
type Error = JsValue;
fn new() -> Result<Self, JsValue> {
@ -168,7 +170,7 @@ impl Location for BrowserUrl {
fn search_params_from_web_url(
params: &web_sys::UrlSearchParams,
) -> Result<Params, JsValue> {
) -> Result<ParamsMap, JsValue> {
try_iter(params)?
.into_iter()
.flatten()

View file

@ -1,7 +1,12 @@
use any_spawner::Executor;
use core::fmt::Debug;
use js_sys::Reflect;
use reactive_graph::signal::ArcRwSignal;
use reactive_graph::{
computed::Memo,
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
traits::With,
};
use send_wrapper::SendWrapper;
use std::{borrow::Cow, future::Future};
use tachys::dom::window;
use wasm_bindgen::{JsCast, JsValue};
@ -9,7 +14,7 @@ use web_sys::{Event, HtmlAnchorElement, MouseEvent};
mod history;
mod server;
use crate::Params;
use crate::params::ParamsMap;
pub use history::*;
pub use server::*;
@ -20,7 +25,7 @@ pub struct Url {
origin: String,
path: String,
search: String,
search_params: Params,
search_params: ParamsMap,
hash: String,
}
@ -37,7 +42,7 @@ impl Url {
&self.search
}
pub fn search_params(&self) -> &Params {
pub fn search_params(&self) -> &ParamsMap {
&self.search_params
}
@ -46,6 +51,39 @@ impl Url {
}
}
/// A reactive description of the current URL, containing equivalents to the local parts of
/// the browser's [`Location`](https://developer.mozilla.org/en-US/docs/Web/API/Location).
#[derive(Debug, Clone, PartialEq)]
pub struct Location {
/// The path of the URL, not containing the query string or hash fragment.
pub pathname: Memo<String>,
/// The raw query string.
pub search: Memo<String>,
/// The query string parsed into its key-value pairs.
pub query: Memo<ParamsMap>,
/// The hash fragment.
pub hash: Memo<String>,
/// The [`state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) at the top of the history stack.
pub state: ReadSignal<State>,
}
impl Location {
pub(crate) fn new(url: ReadSignal<Url>, state: ReadSignal<State>) -> Self {
let pathname = Memo::new(move |_| url.with(|url| url.path.clone()));
let search = Memo::new(move |_| url.with(|url| url.search.clone()));
let hash = Memo::new(move |_| url.with(|url| url.hash.clone()));
let query =
Memo::new(move |_| url.with(|url| url.search_params.clone()));
Location {
pathname,
search,
query,
hash,
state,
}
}
}
/// A description of a navigation.
#[derive(Debug, Clone, PartialEq)]
pub struct LocationChange {
@ -71,7 +109,7 @@ impl Default for LocationChange {
}
}
pub trait Location: Sized {
pub trait LocationProvider: Sized {
type Error: Debug;
fn new() -> Result<Self, Self::Error>;
@ -93,24 +131,40 @@ pub trait Location: Sized {
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error>;
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct State(pub Option<JsValue>);
#[derive(Debug, Clone)]
pub struct State(SendWrapper<Option<JsValue>>);
impl State {
pub fn new(state: Option<JsValue>) -> Self {
Self(SendWrapper::new(state))
}
pub fn to_js_value(&self) -> JsValue {
match &self.0 {
match &*self.0 {
Some(v) => v.clone(),
None => JsValue::UNDEFINED,
}
}
}
impl Default for State {
fn default() -> Self {
Self(SendWrapper::new(None))
}
}
impl PartialEq for State {
fn eq(&self, other: &Self) -> bool {
&*self.0 == &*other.0
}
}
impl<T> From<T> for State
where
T: Into<JsValue>,
{
fn from(value: T) -> Self {
State(Some(value.into()))
State::new(Some(value.into()))
}
}
@ -210,7 +264,7 @@ where
value: to,
replace,
scroll: true,
state: State(state),
state: State::new(state),
};
Executor::spawn_local(navigate(url, change));

View file

@ -1,31 +1,29 @@
use crate::{
location::{Location, Url},
matching::Routes,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Params,
RouteMatchId,
params::ParamsMap,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, RouteMatchId,
};
use either_of::Either;
use leptos::{component, IntoView};
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::ArcMemo,
computed::{ArcMemo, Memo},
owner::{provide_context, use_context, Owner},
signal::{ArcRwSignal, ArcTrigger},
traits::{Read, Set, Track, Trigger},
traits::{Get, Read, Set, Track, Trigger},
};
use std::{
borrow::Cow,
cell::RefCell,
marker::PhantomData,
mem,
rc::Rc,
sync::{
mpsc::{self, Receiver, Sender},
Arc, Mutex,
},
};
use tachys::{
renderer::{dom::Dom, Renderer},
renderer::Renderer,
view::{
any_view::{AnyView, AnyViewState, IntoAny},
either::EitherState,
@ -42,7 +40,7 @@ pub(crate) struct NestedRoutesView<Defs, Fal, R> {
pub outer_owner: Owner,
pub url: ArcRwSignal<Url>,
pub path: ArcMemo<String>,
pub search_params: ArcMemo<Params>,
pub search_params: ArcMemo<ParamsMap>,
pub base: Option<Cow<'static, str>>,
pub fallback: Fal,
pub rndr: PhantomData<R>,
@ -56,8 +54,8 @@ where
outer_owner: Owner,
url: ArcRwSignal<Url>,
path: ArcMemo<String>,
search_params: ArcMemo<Params>,
outlets: Vec<OutletContext<R>>,
search_params: ArcMemo<ParamsMap>,
outlets: Vec<RouteContext<R>>,
view: EitherState<Fal::State, AnyViewState<R>, R>,
}
@ -162,19 +160,29 @@ where
type OutletViewFn<R> = Box<dyn FnOnce() -> AnyView<R> + Send>;
#[derive(Debug)]
pub struct OutletContext<R>
pub struct RouteContext<R>
where
R: Renderer,
{
id: RouteMatchId,
trigger: ArcTrigger,
params: ArcRwSignal<Params>,
params: ArcRwSignal<ParamsMap>,
owner: Owner,
tx: Sender<OutletViewFn<R>>,
rx: Arc<Mutex<Option<Receiver<OutletViewFn<R>>>>>,
}
impl<R> Clone for OutletContext<R>
impl<R> RouteContext<R>
where
R: Renderer + 'static,
{
fn provide_contexts(&self) {
provide_context(self.params.read_only());
provide_context(self.clone());
}
}
impl<R> Clone for RouteContext<R>
where
R: Renderer,
{
@ -196,14 +204,14 @@ where
{
fn build_nested_route(
self,
outlets: &mut Vec<OutletContext<R>>,
outlets: &mut Vec<RouteContext<R>>,
parent: &Owner,
);
fn rebuild_nested_route(
self,
items: &mut usize,
outlets: &mut Vec<OutletContext<R>>,
outlets: &mut Vec<RouteContext<R>>,
parent: &Owner,
);
}
@ -215,7 +223,7 @@ where
{
fn build_nested_route(
self,
outlets: &mut Vec<OutletContext<R>>,
outlets: &mut Vec<RouteContext<R>>,
parent: &Owner,
) {
// each Outlet gets its own owner, so it can inherit context from its parent route,
@ -238,7 +246,7 @@ where
let (tx, rx) = mpsc::channel();
// add this outlet to the end of the outlet stack used for diffing
let outlet = OutletContext {
let outlet = RouteContext {
id: self.as_id(),
trigger,
params,
@ -258,8 +266,8 @@ where
// and share the outlet with the parent via context
// we share it with the *parent* because the <Outlet/> is rendered in or below the parent
// wherever it appears, <Outlet/> will look for the closest OutletContext
parent.with(|| provide_context(outlet));
// wherever it appears, <Outlet/> will look for the closest RouteContext
parent.with(|| outlet.provide_contexts());
// recursively continue building the tree
// this is important because to build the view, we need access to the outlet
@ -272,7 +280,7 @@ where
fn rebuild_nested_route(
self,
items: &mut usize,
outlets: &mut Vec<OutletContext<R>>,
outlets: &mut Vec<RouteContext<R>>,
parent: &Owner,
) {
let current = outlets.get_mut(*items);
@ -292,7 +300,7 @@ where
// 2) access the view and children
current
.params
.set(self.to_params().into_iter().collect::<Params>());
.set(self.to_params().into_iter().collect::<ParamsMap>());
let (view, child) = self.into_view_and_child();
// if the IDs don't match, everything below in the tree needs to be swapped:
@ -376,9 +384,9 @@ where
R: Renderer + 'static,
{
_ = rndr;
let ctx = use_context::<OutletContext<R>>()
.expect("<Outlet/> used without OutletContext being provided.");
let OutletContext {
let ctx = use_context::<RouteContext<R>>()
.expect("<Outlet/> used without RouteContext being provided.");
let RouteContext {
id,
trigger,
params,

View file

@ -1,21 +1,74 @@
use std::borrow::Cow;
use std::{borrow::Cow, str::FromStr, sync::Arc};
use thiserror::Error;
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct Params(Vec<(Cow<'static, str>, String)>);
pub struct ParamsMap(Vec<(Cow<'static, str>, String)>);
impl Params {
impl ParamsMap {
/// Creates an empty map.
#[inline(always)]
pub fn new() -> Self {
Self::default()
}
/// Creates an empty map with the given capacity.
#[inline(always)]
pub fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
/*
/// Inserts a value into the map.
#[inline(always)]
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
use crate::history::url::unescape;
let value = unescape(&value);
self.0.insert(key, value)
}
*/
/// Gets a value from the map.
#[inline(always)]
pub fn get(&self, key: &str) -> Option<&str> {
self.0
.iter()
.find_map(|(k, v)| (k == key).then_some(v.as_str()))
}
/// Removes a value from the map.
#[inline(always)]
pub fn remove(&mut self, key: &str) -> Option<String> {
for i in 0..self.0.len() {
if self.0[i].0 == key {
return Some(self.0.swap_remove(i).1);
}
}
None
}
/*
/// Converts the map to a query string.
pub fn to_query_string(&self) -> String {
use crate::history::url::escape;
let mut buf = String::new();
if !self.0.is_empty() {
buf.push('?');
for (k, v) in &self.0 {
buf.push_str(&escape(k));
buf.push('=');
buf.push_str(&escape(v));
buf.push('&');
}
if buf.len() > 1 {
buf.pop();
}
}
buf
}
*/
}
impl<K, V> FromIterator<(K, V)> for Params
impl<K, V> FromIterator<(K, V)> for ParamsMap
where
K: Into<Cow<'static, str>>,
V: Into<String>,
@ -28,3 +81,91 @@ where
)
}
}
/// A simple method of deserializing key-value data (like route params or URL search)
/// into a concrete data type. `Self` should typically be a struct in which
/// each field's type implements [`FromStr`].
pub trait Params
where
Self: Sized,
{
/// Attempts to deserialize the map into the given type.
fn from_map(map: &ParamsMap) -> Result<Self, ParamsError>;
}
impl Params for () {
#[inline(always)]
fn from_map(_map: &ParamsMap) -> Result<Self, ParamsError> {
Ok(())
}
}
pub trait IntoParam
where
Self: Sized,
{
fn into_param(value: Option<&str>, name: &str)
-> Result<Self, ParamsError>;
}
impl<T> IntoParam for Option<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(
value: Option<&str>,
_name: &str,
) -> Result<Self, ParamsError> {
match value {
None => Ok(None),
Some(value) => match T::from_str(value) {
Ok(value) => Ok(Some(value)),
Err(e) => Err(ParamsError::Params(Arc::new(e))),
},
}
}
}
// TODO can we support Option<T> and T in a non-nightly way?
#[cfg(feature = "nightly")]
mod option_param {
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}
impl<T> IntoParam for T
where
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(
value: Option<&str>,
name: &str,
) -> Result<Self, ParamsError> {
let value = value
.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
}
}
}
/// Errors that can occur while parsing params using [`Params`].
#[derive(Error, Debug, Clone)]
pub enum ParamsError {
/// A field was missing from the route params.
#[error("could not find parameter {0}")]
MissingParam(String),
/// Something went wrong while deserializing a field.
#[error("failed to deserialize parameters")]
Params(Arc<dyn std::error::Error + Send + Sync>),
}
impl PartialEq for ParamsError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::MissingParam(l0), Self::MissingParam(r0)) => l0 == r0,
(Self::Params(_), Self::Params(_)) => false,
_ => false,
}
}
}