Make necessary changes for stable support for router and meta

This commit is contained in:
Greg Johnston 2022-12-05 18:55:03 -05:00
parent 7f696a9ac4
commit 5c45538e9f
10 changed files with 278 additions and 159 deletions

View file

@ -1,5 +1,6 @@
use std::time::Duration; use std::time::Duration;
use cfg_if::cfg_if;
use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt}; use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
@ -254,30 +255,58 @@ pub fn set_interval(
Ok(IntervalHandle(handle)) Ok(IntervalHandle(handle))
} }
/// Adds an event listener to the target DOM element using implicit event delegation. cfg_if! {
pub fn add_event_listener<E>( if #[cfg(not(feature = "stable"))] {
/// Adds an event listener to the target DOM element using implicit event delegation.
pub fn add_event_listener<E>(
target: &web_sys::Element, target: &web_sys::Element,
event_name: &'static str, event_name: &'static str,
cb: impl FnMut(E) + 'static, cb: impl FnMut(E) + 'static,
) where ) where
E: FromWasmAbi + 'static, E: FromWasmAbi + 'static,
{ {
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value(); let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let key = event_delegation::event_delegation_key(event_name); let key = event_delegation::event_delegation_key(event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb); _ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
event_delegation::add_event_listener(event_name); event_delegation::add_event_listener(event_name);
} }
#[doc(hidden)] #[doc(hidden)]
pub fn add_event_listener_undelegated<E>( pub fn add_event_listener_undelegated<E>(
target: &web_sys::Element, target: &web_sys::Element,
event_name: &'static str, event_name: &'static str,
cb: impl FnMut(E) + 'static, cb: impl FnMut(E) + 'static,
) where ) where
E: FromWasmAbi + 'static, E: FromWasmAbi + 'static,
{ {
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value(); let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref()); _ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
} else {
/// Adds an event listener to the target DOM element using implicit event delegation.
pub fn add_event_listener(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(web_sys::Event) + 'static,
)
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
let key = event_delegation::event_delegation_key(event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
event_delegation::add_event_listener(event_name);
}
#[doc(hidden)]
pub fn add_event_listener_undelegated(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(web_sys::Event) + 'static,
)
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
}
} }
#[doc(hidden)] #[doc(hidden)]

View file

@ -564,6 +564,18 @@ fn attr_to_tokens(
let event_type = event_type.parse::<TokenStream>().expect("couldn't parse event name"); let event_type = event_type.parse::<TokenStream>().expect("couldn't parse event name");
if mode != Mode::Ssr { if mode != Mode::Ssr {
cfg_if::cfg_if! {
if #[cfg(feature = "stable")] {
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated(#el_id.unchecked_ref(), #name, #handler);
});
} else {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener(#el_id.unchecked_ref(), #name, #handler);
});
}
} else {
if NON_BUBBLING_EVENTS.contains(&name.as_str()) { if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! { expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler); span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
@ -573,6 +585,8 @@ fn attr_to_tokens(
span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler); span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
}); });
} }
}
}
} else { } else {
// this is here to avoid warnings about unused signals // this is here to avoid warnings about unused signals

View file

@ -1,6 +1,6 @@
[package] [package]
name = "leptos_meta" name = "leptos_meta"
version = "0.0.4" version = "0.0.5"
edition = "2021" edition = "2021"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "leptos_router" name = "leptos_router"
version = "0.0.5" version = "0.0.6"
edition = "2021" edition = "2021"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"
@ -58,7 +58,8 @@ default = ["csr"]
csr = ["leptos/csr"] csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"] hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr", "dep:url", "dep:regex"] ssr = ["leptos/ssr", "dep:url", "dep:regex"]
stable = ["leptos/stable"]
[package.metadata.cargo-all-features] [package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature # No need to test optional dependencies as they are enabled by the ssr feature
denylist = ["url", "regex"] denylist = ["url", "regex", "stable"]

View file

@ -1,4 +1,5 @@
use crate::{use_navigate, use_resolved_path, TextProp}; use crate::{use_navigate, use_resolved_path, TextProp};
use cfg_if::cfg_if;
use leptos::*; use leptos::*;
use std::{error::Error, rc::Rc}; use std::{error::Error, rc::Rc};
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
@ -131,6 +132,14 @@ where
let children = children(); let children = children();
cfg_if! {
if #[cfg(feature = "stable")] {
let on_submit = move |ev: web_sys::Event| on_submit(ev.unchecked_into());
}
};
cfg_if! {
if #[cfg(not(feature = "stable"))] {
view! { cx, view! { cx,
<form <form
method=method method=method
@ -141,6 +150,20 @@ where
{children} {children}
</form> </form>
} }
}
else {
view! { cx,
<form
method=method
action=move || action.get()
enctype=enctype
on:submit=on_submit
>
{children}
</form>
}
}
}
} }
/// Properties that can be passed to the [ActionForm] component, which /// Properties that can be passed to the [ActionForm] component, which
@ -282,6 +305,12 @@ where
let children = (props.children)(); let children = (props.children)();
cfg_if! {
if #[cfg(feature = "stable")] {
let on_submit = move |ev: web_sys::Event| on_submit(ev.unchecked_into());
}
};
view! { cx, view! { cx,
<form <form
method="POST" method="POST"

View file

@ -68,7 +68,6 @@ impl std::fmt::Debug for RouterContextInner {
f.debug_struct("RouterContextInner") f.debug_struct("RouterContextInner")
.field("location", &self.location) .field("location", &self.location)
.field("base", &self.base) .field("base", &self.base)
.field("history", &std::any::type_name_of_val(&self.history))
.field("cx", &self.cx) .field("cx", &self.cx)
.field("reference", &self.reference) .field("reference", &self.reference)
.field("set_reference", &self.set_reference) .field("set_reference", &self.set_reference)
@ -103,14 +102,16 @@ impl RouterContext {
let base = base.unwrap_or_default(); let base = base.unwrap_or_default();
let base_path = resolve_path("", base, None); let base_path = resolve_path("", base, None);
if let Some(base_path) = &base_path && source.with(|s| s.value.is_empty()) { if let Some(base_path) = &base_path {
if source.with(|s| s.value.is_empty()) {
history.navigate(&LocationChange { history.navigate(&LocationChange {
value: base_path.to_string(), value: base_path.to_string(),
replace: true, replace: true,
scroll: false, scroll: false,
state: State(None) state: State(None),
}); });
} }
}
// the current URL // the current URL
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone())); let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
@ -136,9 +137,9 @@ impl RouterContext {
// 3) update the state // 3) update the state
// this will trigger the new route match below // this will trigger the new route match below
create_render_effect(cx, move |_| { create_render_effect(cx, move |_| {
let LocationChange { value, state, .. } = source(); let LocationChange { value, state, .. } = source.get();
cx.untrack(move || { cx.untrack(move || {
if value != reference() { if value != reference.get() {
set_reference.update(move |r| *r = value); set_reference.update(move |r| *r = value);
set_state.update(move |s| *s = state); set_state.update(move |s| *s = state);
} }

View file

@ -1,9 +1,20 @@
use std::{cmp::Reverse, rc::Rc, cell::{RefCell, Cell}, ops::IndexMut}; use std::{
cell::{Cell, RefCell},
cmp::Reverse,
ops::IndexMut,
rc::Rc,
};
use leptos::*; use leptos::*;
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
use crate::{matching::{expand_optionals, join_paths, Branch, Matcher, RouteDefinition, get_route_matches, RouteMatch}, RouterContext, RouteContext}; use crate::{
matching::{
expand_optionals, get_route_matches, join_paths, Branch, Matcher, RouteDefinition,
RouteMatch,
},
RouteContext, RouterContext,
};
/// Props for the [Routes] component, which contains route definitions and manages routing. /// Props for the [Routes] component, which contains route definitions and manages routing.
#[derive(TypedBuilder)] #[derive(TypedBuilder)]
@ -34,9 +45,7 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
// whenever path changes, update matches // whenever path changes, update matches
let matches = create_memo(cx, { let matches = create_memo(cx, {
let router = router.clone(); let router = router.clone();
move |_| { move |_| get_route_matches(branches.clone(), router.pathname().get())
get_route_matches(branches.clone(), router.pathname().get())
}
}); });
// Rebuild the list of nested routes conservatively, and show the root route here // Rebuild the list of nested routes conservatively, and show the root route here
@ -68,14 +77,18 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
let prev_match = prev_matches.and_then(|p| p.get(i)); let prev_match = prev_matches.and_then(|p| p.get(i));
let next_match = next_matches.get(i).unwrap(); let next_match = next_matches.get(i).unwrap();
if let Some(prev) = prev_routes && let Some(prev_match) = prev_match && next_match.route.key == prev_match.route.key { match (prev_routes, prev_match) {
(Some(prev), Some(prev_match))
if next_match.route.key == prev_match.route.key =>
{
let prev_one = { prev.borrow()[i].clone() }; let prev_one = { prev.borrow()[i].clone() };
if i >= next.borrow().len() { if i >= next.borrow().len() {
next.borrow_mut().push(prev_one); next.borrow_mut().push(prev_one);
} else { } else {
*(next.borrow_mut().index_mut(i)) = prev_one; *(next.borrow_mut().index_mut(i)) = prev_one;
} }
} else { }
_ => {
equal = false; equal = false;
if i == 0 { if i == 0 {
root_equal.set(false); root_equal.set(false);
@ -92,7 +105,9 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
{ {
let next = next.clone(); let next = next.clone();
move || { move || {
if let Some(route_states) = use_context::<Memo<RouterState>>(cx) { if let Some(route_states) =
use_context::<Memo<RouterState>>(cx)
{
route_states.with(|route_states| { route_states.with(|route_states| {
let routes = route_states.routes.borrow(); let routes = route_states.routes.borrow();
routes.get(i + 1).cloned() routes.get(i + 1).cloned()
@ -102,9 +117,7 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
} }
} }
}, },
move || { move || matches.with(|m| m.get(i).cloned()),
matches.with(|m| m.get(i).cloned())
}
); );
if let Some(next_ctx) = next_ctx { if let Some(next_ctx) = next_ctx {
@ -126,6 +139,7 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
} }
} }
} }
}
if disposers.borrow().len() > next_matches.len() { if disposers.borrow().len() > next_matches.len() {
let surplus_disposers = disposers.borrow_mut().split_off(next_matches.len() + 1); let surplus_disposers = disposers.borrow_mut().split_off(next_matches.len() + 1);
@ -134,7 +148,8 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
} }
} }
if let Some(prev) = &prev && equal { if let Some(prev) = &prev {
if equal {
RouterState { RouterState {
matches: next_matches.to_vec(), matches: next_matches.to_vec(),
routes: prev_routes.cloned().unwrap_or_default(), routes: prev_routes.cloned().unwrap_or_default(),
@ -145,14 +160,22 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
RouterState { RouterState {
matches: next_matches.to_vec(), matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())), routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root root,
}
}
} else {
let root = next.borrow().get(0).cloned();
RouterState {
matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root,
} }
} }
} }
}); });
// show the root route // show the root route
create_memo(cx, move |prev| { let root = create_memo(cx, move |prev| {
provide_context(cx, route_states); provide_context(cx, route_states);
route_states.with(|state| { route_states.with(|state| {
let root = state.routes.borrow(); let root = state.routes.borrow();
@ -162,14 +185,20 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
} }
if prev.is_none() || !root_equal.get() { if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| { root.as_ref().map(|route| route.outlet().into_child(cx))
route.outlet().into_child(cx)
})
} else { } else {
prev.cloned().unwrap() prev.cloned().unwrap()
} }
}) })
}) });
cfg_if::cfg_if! {
if #[cfg(feature = "stable")] {
move || root.get()
} else {
root
}
}
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View file

@ -107,11 +107,27 @@ where
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError>; fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError>;
} }
impl<T> IntoParam for Option<T> cfg_if::cfg_if! {
where if #[cfg(not(feature = "stable"))] {
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(Rc::new(e)))
}
}
impl<T> IntoParam for Option<T>
where
T: FromStr, T: FromStr,
<T as FromStr>::Err: std::error::Error + 'static, <T as FromStr>::Err: std::error::Error + 'static,
{ {
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> { fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
match value { match value {
None => Ok(None), None => Ok(None),
@ -124,20 +140,19 @@ where
}, },
} }
} }
} }
} else {
auto trait NotOption {} impl<T> IntoParam for T
impl<T> !NotOption for Option<T> {} where
T: FromStr,
impl<T> IntoParam for T
where
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static, <T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{ {
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> { fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?; let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e))) Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
} }
}
}
} }
/// Errors that can occur while parsing params using [Params](crate::Params). /// Errors that can occur while parsing params using [Params](crate::Params).

View file

@ -138,10 +138,9 @@
//! //!
//! ``` //! ```
#![feature(auto_traits)] #![cfg_attr(not(feature = "stable"), feature(auto_traits))]
#![feature(let_chains)] #![cfg_attr(not(feature = "stable"), feature(negative_impls))]
#![feature(negative_impls)] #![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
#![feature(type_name_of_val)]
mod components; mod components;
mod history; mod history;

View file

@ -80,7 +80,8 @@ impl Matcher {
path.push_str(loc_segment); path.push_str(loc_segment);
} }
if let Some(splat) = &self.splat && !splat.is_empty() { if let Some(splat) = &self.splat {
if !splat.is_empty() {
let value = if len_diff > 0 { let value = if len_diff > 0 {
loc_segments[self.len..].join("/") loc_segments[self.len..].join("/")
} else { } else {
@ -88,6 +89,7 @@ impl Matcher {
}; };
params.insert(splat.into(), value); params.insert(splat.into(), value);
} }
}
Some(PathMatch { path, params }) Some(PathMatch { path, params })
} }