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"))] {
target: &web_sys::Element, /// Adds an event listener to the target DOM element using implicit event delegation.
event_name: &'static str, pub fn add_event_listener<E>(
cb: impl FnMut(E) + 'static, target: &web_sys::Element,
) where event_name: &'static str,
E: FromWasmAbi + 'static, cb: impl FnMut(E) + 'static,
{ ) where
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value(); E: FromWasmAbi + 'static,
let key = event_delegation::event_delegation_key(event_name); {
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb); let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
event_delegation::add_event_listener(event_name); 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)] #[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,14 +564,28 @@ 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 {
if NON_BUBBLING_EVENTS.contains(&name.as_str()) { cfg_if::cfg_if! {
expressions.push(quote_spanned! { if #[cfg(feature = "stable")] {
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler); if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
}); expressions.push(quote_spanned! {
} else { span => ::leptos::add_event_listener_undelegated(#el_id.unchecked_ref(), #name, #handler);
expressions.push(quote_spanned! { });
span => ::leptos::add_event_listener::<web_sys::#event_type>(#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()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
} else {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
}
}
} }
} else { } else {

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,15 +132,37 @@ where
let children = children(); let children = children();
view! { cx, cfg_if! {
<form if #[cfg(feature = "stable")] {
method=method let on_submit = move |ev: web_sys::Event| on_submit(ev.unchecked_into());
action=action }
enctype=enctype };
on:submit=on_submit
> cfg_if! {
{children} if #[cfg(not(feature = "stable"))] {
</form> view! { cx,
<form
method=method
action=action
enctype=enctype
on:submit=on_submit
>
{children}
</form>
}
}
else {
view! { cx,
<form
method=method
action=move || action.get()
enctype=enctype
on:submit=on_submit
>
{children}
</form>
}
}
} }
} }
@ -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 {
history.navigate(&LocationChange { if source.with(|s| s.value.is_empty()) {
value: base_path.to_string(), history.navigate(&LocationChange {
replace: true, value: base_path.to_string(),
scroll: false, replace: true,
state: State(None) scroll: false,
}); 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,61 +77,66 @@ 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) {
let prev_one = { prev.borrow()[i].clone() }; (Some(prev), Some(prev_match))
if i >= next.borrow().len() { if next_match.route.key == prev_match.route.key =>
next.borrow_mut().push(prev_one); {
} else { let prev_one = { prev.borrow()[i].clone() };
*(next.borrow_mut().index_mut(i)) = prev_one; if i >= next.borrow().len() {
} next.borrow_mut().push(prev_one);
} else { } else {
equal = false; *(next.borrow_mut().index_mut(i)) = prev_one;
if i == 0 { }
root_equal.set(false);
} }
_ => {
equal = false;
if i == 0 {
root_equal.set(false);
}
let disposer = cx.child_scope({ let disposer = cx.child_scope({
let next = next.clone();
let router = Rc::clone(&router.inner);
move |cx| {
let next = next.clone(); let next = next.clone();
let next_ctx = RouteContext::new( let router = Rc::clone(&router.inner);
cx, move |cx| {
&RouterContext { inner: router }, let next = next.clone();
{ let next_ctx = RouteContext::new(
let next = next.clone(); cx,
move || { &RouterContext { inner: router },
if let Some(route_states) = use_context::<Memo<RouterState>>(cx) { {
route_states.with(|route_states| { let next = next.clone();
let routes = route_states.routes.borrow(); move || {
routes.get(i + 1).cloned() if let Some(route_states) =
}) use_context::<Memo<RouterState>>(cx)
} else { {
next.borrow().get(i + 1).cloned() route_states.with(|route_states| {
let routes = route_states.routes.borrow();
routes.get(i + 1).cloned()
})
} else {
next.borrow().get(i + 1).cloned()
}
} }
} },
}, move || matches.with(|m| m.get(i).cloned()),
move || { );
matches.with(|m| m.get(i).cloned())
}
);
if let Some(next_ctx) = next_ctx { if let Some(next_ctx) = next_ctx {
if next.borrow().len() > i + 1 { if next.borrow().len() > i + 1 {
next.borrow_mut()[i] = next_ctx; next.borrow_mut()[i] = next_ctx;
} else { } else {
next.borrow_mut().push(next_ctx); next.borrow_mut().push(next_ctx);
}
} }
} }
} });
});
if disposers.borrow().len() > i + 1 { if disposers.borrow().len() > i + 1 {
let mut disposers = disposers.borrow_mut(); let mut disposers = disposers.borrow_mut();
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer); let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
old_route_disposer.dispose(); old_route_disposer.dispose();
} else { } else {
disposers.borrow_mut().push(disposer); disposers.borrow_mut().push(disposer);
}
} }
} }
} }
@ -134,25 +148,34 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
} }
} }
if let Some(prev) = &prev && equal { if let Some(prev) = &prev {
RouterState { if equal {
matches: next_matches.to_vec(), RouterState {
routes: prev_routes.cloned().unwrap_or_default(), matches: next_matches.to_vec(),
root: prev.root.clone(), routes: prev_routes.cloned().unwrap_or_default(),
root: prev.root.clone(),
}
} else {
let root = next.borrow().get(0).cloned();
RouterState {
matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root,
}
} }
} else { } else {
let root = next.borrow().get(0).cloned(); let root = next.borrow().get(0).cloned();
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,
} }
} }
} }
}); });
// 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,36 +107,51 @@ 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"))] {
T: FromStr, auto trait NotOption {}
<T as FromStr>::Err: std::error::Error + 'static, impl<T> !NotOption for Option<T> {}
{
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> { impl<T> IntoParam for T
match value { where
None => Ok(None), T: FromStr + NotOption,
Some(value) => match T::from_str(value) { <T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
Ok(value) => Ok(Some(value)), {
Err(e) => { fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
eprintln!("{}", e); let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Err(ParamsError::Params(Rc::new(e))) Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
} }
},
} }
}
}
auto trait NotOption {} impl<T> IntoParam for Option<T>
impl<T> !NotOption for Option<T> {} where
T: FromStr,
impl<T> IntoParam for T <T as FromStr>::Err: std::error::Error + 'static,
where {
T: FromStr + NotOption, fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static, match value {
{ None => Ok(None),
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> { Some(value) => match T::from_str(value) {
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?; Ok(value) => Ok(Some(value)),
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e))) Err(e) => {
eprintln!("{}", e);
Err(ParamsError::Params(Rc::new(e)))
}
},
}
}
}
} else {
impl<T> IntoParam for 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> {
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
}
}
} }
} }

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,13 +80,15 @@ 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 {
let value = if len_diff > 0 { if !splat.is_empty() {
loc_segments[self.len..].join("/") let value = if len_diff > 0 {
} else { loc_segments[self.len..].join("/")
"".into() } else {
}; "".into()
params.insert(splat.into(), value); };
params.insert(splat.into(), value);
}
} }
Some(PathMatch { path, params }) Some(PathMatch { path, params })