mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
add more hooks and primitives to router
This commit is contained in:
parent
12f2cec5c7
commit
2dd2bb5958
11 changed files with 665 additions and 177 deletions
|
@ -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 || {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,)*
|
||||
})
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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
266
routing/src/hooks.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -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::*;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue