From e4c910927861164711723a1d584d7468ac6a4eba Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 20 Jan 2023 09:13:18 -0300 Subject: [PATCH 1/4] Fix query params behaviour difference between SSR and Hydrate --- router/Cargo.toml | 2 +- router/src/history/location.rs | 2 +- router/src/history/url.rs | 67 ++++++++++------------------------ 3 files changed, 21 insertions(+), 50 deletions(-) diff --git a/router/Cargo.toml b/router/Cargo.toml index 94b7a3cd8..953e9fde6 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -56,7 +56,7 @@ features = [ [features] default = [] csr = ["leptos/csr"] -hydrate = ["leptos/hydrate"] +hydrate = ["leptos/hydrate", "dep:url"] ssr = ["leptos/ssr", "dep:url", "dep:regex"] stable = ["leptos/stable"] diff --git a/router/src/history/location.rs b/router/src/history/location.rs index 77ef91b5e..a9b40ac5f 100644 --- a/router/src/history/location.rs +++ b/router/src/history/location.rs @@ -19,7 +19,7 @@ pub fn create_location(cx: Scope, path: ReadSignal, state: ReadSignal ParamsMap { - let map = self - .search - .trim_start_matches('?') - .split('&') - .filter_map(|piece| { - let mut parts = piece.split('='); - let (k, v) = (parts.next(), parts.next()); - match k { - Some(k) if !k.is_empty() => { - Some((unescape(k), unescape(v.unwrap_or_default()))) - } - _ => None, - } - }) - .collect::>(); - ParamsMap(map) - } -} - -#[cfg(feature = "ssr")] -pub fn unescape(s: &str) -> String { - urlencoding::decode(s) - .unwrap_or_else(|_| std::borrow::Cow::from(s)) - .replace('+', " ") -} - #[cfg(not(feature = "ssr"))] pub fn unescape(s: &str) -> String { js_sys::decode_uri(s).unwrap().into() @@ -51,34 +25,31 @@ pub fn escape(s: &str) -> String { js_sys::encode_uri(s).as_string().unwrap() } -#[cfg(not(feature = "ssr"))] impl TryFrom<&str> for Url { type Error = String; fn try_from(url: &str) -> Result { - let fake_host = String::from("http://leptos"); - let url = web_sys::Url::new_with_base(url, &fake_host) - .map_err(|e| e.as_string().unwrap_or_default())?; + let url = url::Url::parse(&normalize_wasm_url(url)).map_err(|e| e.to_string())?; Ok(Self { - origin: url.origin(), - pathname: url.pathname(), - search: url.search(), - hash: url.hash(), + origin: url.origin().unicode_serialization(), + pathname: url.path().to_string(), + search: url.query().unwrap_or_default().to_string(), + search_params: ParamsMap( + url.query_pairs() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect::>(), + ), + hash: Default::default(), }) } } #[cfg(feature = "ssr")] -impl TryFrom<&str> for Url { - type Error = String; - - fn try_from(url: &str) -> Result { - let url = url::Url::parse(url).map_err(|e| e.to_string())?; - Ok(Self { - origin: url.origin().unicode_serialization(), - pathname: url.path().to_string(), - search: url.query().unwrap_or_default().to_string(), - hash: Default::default(), - }) - } +fn normalize_wasm_url(url: &str) -> Cow<'_, str> { + Cow::Borrowed(url) +} + +#[cfg(not(feature = "ssr"))] +fn normalize_wasm_url(url: &str) -> Cow<'_, str> { + Cow::Owned(format!("http://leptos{}", url)) } From 18eecd960608fa328f56618ef416168f5e2bf66d Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 20 Jan 2023 18:11:49 -0300 Subject: [PATCH 2/4] Use URLSearchParams to handle client side query param logic --- router/Cargo.toml | 2 +- router/src/history/url.rs | 51 +++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/router/Cargo.toml b/router/Cargo.toml index 953e9fde6..94b7a3cd8 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -56,7 +56,7 @@ features = [ [features] default = [] csr = ["leptos/csr"] -hydrate = ["leptos/hydrate", "dep:url"] +hydrate = ["leptos/hydrate"] ssr = ["leptos/ssr", "dep:url", "dep:regex"] stable = ["leptos/stable"] diff --git a/router/src/history/url.rs b/router/src/history/url.rs index cf5bc6565..b46f735c9 100644 --- a/router/src/history/url.rs +++ b/router/src/history/url.rs @@ -1,5 +1,10 @@ use crate::ParamsMap; -use std::borrow::Cow; +#[cfg(not(feature = "ssr"))] +use js_sys::{try_iter, Array, JsString}; +#[cfg(not(feature = "ssr"))] +use wasm_bindgen::JsCast; +#[cfg(not(feature = "ssr"))] +use wasm_bindgen::JsValue; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Url { @@ -25,11 +30,41 @@ pub fn escape(s: &str) -> String { js_sys::encode_uri(s).as_string().unwrap() } +#[cfg(not(feature = "ssr"))] impl TryFrom<&str> for Url { type Error = String; fn try_from(url: &str) -> Result { - let url = url::Url::parse(&normalize_wasm_url(url)).map_err(|e| e.to_string())?; + let fake_host = String::from("http://leptos"); + let url = web_sys::Url::new_with_base(url, &fake_host).map_js_error()?; + Ok(Self { + origin: url.origin(), + pathname: url.pathname(), + search: url.search(), + search_params: ParamsMap( + try_iter(&url.search_params()) + .map_js_error()? + .ok_or("Failed to use URLSearchParams as an iterator".to_string())? + .map(|value| { + let array: Array = value.map_js_error()?.dyn_into().map_js_error()?; + Ok(( + array.get(0).dyn_into::().map_js_error()?.into(), + array.get(1).dyn_into::().map_js_error()?.into(), + )) + }) + .collect::, Self::Error>>()?, + ), + hash: url.hash(), + }) + } +} + +#[cfg(feature = "ssr")] +impl TryFrom<&str> for Url { + type Error = String; + + fn try_from(url: &str) -> Result { + let url = url::Url::parse(url).map_err(|e| e.to_string())?; Ok(Self { origin: url.origin().unicode_serialization(), pathname: url.path().to_string(), @@ -44,12 +79,14 @@ impl TryFrom<&str> for Url { } } -#[cfg(feature = "ssr")] -fn normalize_wasm_url(url: &str) -> Cow<'_, str> { - Cow::Borrowed(url) +#[cfg(not(feature = "ssr"))] +trait MapJsError { + fn map_js_error(self) -> Result; } #[cfg(not(feature = "ssr"))] -fn normalize_wasm_url(url: &str) -> Cow<'_, str> { - Cow::Owned(format!("http://leptos{}", url)) +impl MapJsError for Result { + fn map_js_error(self) -> Result { + self.map_err(|e| e.as_string().unwrap_or_default()) + } } From dbccf525ac5fe9c2a11eba0b3899677ab1c67670 Mon Sep 17 00:00:00 2001 From: Clemente Date: Sat, 21 Jan 2023 11:17:25 -0300 Subject: [PATCH 3/4] Added some tests --- router/src/history/params.rs | 2 +- router/tests/matcher.rs | 6 ++--- router/tests/query_params.rs | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 router/tests/query_params.rs diff --git a/router/src/history/params.rs b/router/src/history/params.rs index 26d2a7516..05cd6f269 100644 --- a/router/src/history/params.rs +++ b/router/src/history/params.rs @@ -78,7 +78,7 @@ macro_rules! params_map { let start_capacity = common_macros::const_expr_count!($($key);*); #[allow(unused_mut)] let mut map = linear_map::LinearMap::with_capacity(start_capacity); - $( map.insert($key, $val); )* + $( map.insert($key.to_string(), $val.to_string()); )* $crate::ParamsMap(map) }); } diff --git a/router/tests/matcher.rs b/router/tests/matcher.rs index 6ab551fdb..0afd90879 100644 --- a/router/tests/matcher.rs +++ b/router/tests/matcher.rs @@ -36,7 +36,7 @@ cfg_if! { Some(PathMatch { path: "/foo/abc-123".into(), params: params_map!( - "id".into() => "abc-123".into() + "id" => "abc-123" ) }) ); @@ -72,7 +72,7 @@ cfg_if! { Some(PathMatch { path: "/foo/bar".into(), params: params_map!( - "something".into() => "baz/qux".into() + "something" => "baz/qux" ) }) ); @@ -87,7 +87,7 @@ cfg_if! { Some(PathMatch { path: "/foo/bar".into(), params: params_map!( - "something".into() => "".into() + "something" => "" ) }) ); diff --git a/router/tests/query_params.rs b/router/tests/query_params.rs new file mode 100644 index 000000000..7f7d43028 --- /dev/null +++ b/router/tests/query_params.rs @@ -0,0 +1,52 @@ +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + use leptos_router::{Url, params_map}; + + macro_rules! assert_params_map { + ([$($key:expr => $val:expr),*] , $actual:expr) => ( + assert_eq!(params_map!($($key => $val),*), $actual) + ); + } + + #[test] + fn test_param_with_plus_sign() { + let url = Url::try_from("http://leptos.com?data=1%2B2%3D3").unwrap(); + assert_params_map!{ + ["data" => "1+2=3"], + url.search_params + }; + } + + #[test] + fn test_param_with_ampersand() { + let url = Url::try_from("http://leptos.com?data=true+%26+false+%3D+false").unwrap(); + assert_params_map!{ + ["data" => "true & false = false"], + url.search_params + }; + } + + #[test] + fn test_complext_query_string() { + let url = Url::try_from("http://leptos.com?data=Data%3A+%24+%26+%2B%2B+7").unwrap(); + assert_params_map!{ + ["data" => "Data: $ & ++ 7"], + url.search_params + }; + } + + #[test] + fn test_multiple_query_params() { + let url = Url::try_from("http://leptos.com?param1=value1¶m2=value2").unwrap(); + assert_params_map!{ + [ + "param1" => "value1", + "param2" => "value2" + ], + url.search_params + }; + } + } +} From cbb1e4c9d29722ff0ae119b1ce7b1a7486045afd Mon Sep 17 00:00:00 2001 From: Clemente Date: Sat, 21 Jan 2023 11:19:28 -0300 Subject: [PATCH 4/4] Update docs --- router/src/history/params.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router/src/history/params.rs b/router/src/history/params.rs index 05cd6f269..57bf33a0c 100644 --- a/router/src/history/params.rs +++ b/router/src/history/params.rs @@ -61,7 +61,7 @@ impl Default for ParamsMap { /// ``` /// # use leptos_router::params_map; /// let map = params_map! { -/// "id".to_string() => "1".to_string() +/// "id" => "1" /// }; /// assert_eq!(map.get("id"), Some(&"1".to_string())); /// assert_eq!(map.get("missing"), None)