diff --git a/packages/router-macro/src/layout.rs b/packages/router-macro/src/layout.rs index 859ef6c6d..18d273c9a 100644 --- a/packages/router-macro/src/layout.rs +++ b/packages/router-macro/src/layout.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::Ident; +use syn::Path; use crate::nest::{Nest, NestId}; @@ -9,8 +9,8 @@ pub struct LayoutId(pub usize); #[derive(Debug)] pub struct Layout { - pub comp: Ident, - pub props_name: Ident, + pub comp: Path, + pub props_name: Path, pub active_nests: Vec, } @@ -38,13 +38,20 @@ impl Layout { pub fn parse(input: syn::parse::ParseStream, active_nests: Vec) -> syn::Result { // Then parse the component name let _ = input.parse::(); - let comp: Ident = input.parse()?; + let comp: Path = input.parse()?; // Then parse the props name let _ = input.parse::(); - let props_name: Ident = input - .parse() - .unwrap_or_else(|_| format_ident!("{}Props", comp.to_string())); + let props_name = input.parse::().unwrap_or_else(|_| { + let last = format_ident!("{}Props", comp.segments.last().unwrap().ident.to_string()); + let mut segments = comp.segments.clone(); + segments.pop(); + segments.push(last.into()); + Path { + leading_colon: None, + segments, + } + }); Ok(Self { comp, diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index bde212e42..381627571 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -57,8 +57,6 @@ pub fn routable(input: TokenStream) -> TokenStream { dioxus_router::prelude::use_generic_router::(cx) } - #route_enum - #error_type #parse_impl @@ -296,9 +294,13 @@ impl RouteEnum { type Err = dioxus_router::routable::RouteParseError<#error_name>; fn from_str(s: &str) -> Result { - let route = s.strip_prefix('/').unwrap_or(s); + let route = s; let (route, query) = route.split_once('?').unwrap_or((route, "")); let mut segments = route.split('/'); + // skip the first empty segment + if s.starts_with('/') { + segments.next(); + } let mut errors = Vec::new(); #(#tokens)* @@ -427,21 +429,6 @@ impl RouteEnum { } } -impl ToTokens for RouteEnum { - fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) { - let routes = &self.routes; - - tokens.extend(quote!( - - #[path = "pages"] - mod pages { - #(#routes)* - } - pub use pages::*; - )); - } -} - struct SiteMapSegment { pub segment_type: SegmentType, pub children: Vec, diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index c7ad4b0d1..691f98e8e 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -1,6 +1,8 @@ -use quote::{format_ident, quote, ToTokens}; +use quote::{format_ident, quote}; use syn::parse::Parse; use syn::parse::ParseStream; +use syn::parse_quote; +use syn::Path; use syn::{Ident, LitStr}; use proc_macro2::TokenStream as TokenStream2; @@ -16,8 +18,8 @@ use crate::segment::RouteSegment; struct RouteArgs { route: LitStr, - comp_name: Option, - props_name: Option, + comp_name: Option, + props_name: Option, } impl Parse for RouteArgs { @@ -40,10 +42,9 @@ impl Parse for RouteArgs { #[derive(Debug)] pub struct Route { - pub file_based: bool, pub route_name: Ident, - pub comp_name: Ident, - pub props_name: Ident, + pub comp_name: Path, + pub props_name: Path, pub route: String, pub segments: Vec, pub query: Option, @@ -73,13 +74,20 @@ impl Route { let route_name = variant.ident.clone(); let args = route_attr.parse_args::()?; let route = args.route.value(); - let file_based = args.comp_name.is_none(); - let comp_name = args - .comp_name - .unwrap_or_else(|| format_ident!("{}", route_name)); - let props_name = args - .props_name - .unwrap_or_else(|| format_ident!("{}Props", comp_name)); + let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name)); + let props_name = args.props_name.unwrap_or_else(|| { + let last = format_ident!( + "{}Props", + comp_name.segments.last().unwrap().ident.to_string() + ); + let mut segments = comp_name.segments.clone(); + segments.pop(); + segments.push(last.into()); + Path { + leading_colon: None, + segments, + } + }); let named_fields = match &variant.fields { syn::Fields::Named(fields) => fields, @@ -106,7 +114,6 @@ impl Route { route_name, segments: route_segments, route, - file_based, query, nests, layouts, @@ -234,35 +241,3 @@ impl Route { } } } - -impl ToTokens for Route { - fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) { - if !self.file_based { - return; - } - - let without_leading_slash = &self.route[1..]; - let route_path = std::path::Path::new(without_leading_slash); - let with_extension = route_path.with_extension("rs"); - let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let dir = std::path::Path::new(&dir); - let route = dir.join("src").join("pages").join(with_extension.clone()); - - // check if the route exists or if not use the index route - let route = if route.exists() && !without_leading_slash.is_empty() { - with_extension.to_str().unwrap().to_string() - } else { - route_path.join("index.rs").to_str().unwrap().to_string() - }; - - let route_name: Ident = self.route_name.clone(); - let prop_name = &self.props_name; - - tokens.extend(quote!( - #[path = #route] - #[allow(non_snake_case)] - mod #route_name; - pub use #route_name::{#prop_name, #route_name}; - )); - } -} diff --git a/packages/router-macro/src/route_tree.rs b/packages/router-macro/src/route_tree.rs index 5a05d3a06..5790c8386 100644 --- a/packages/router-macro/src/route_tree.rs +++ b/packages/router-macro/src/route_tree.rs @@ -291,13 +291,21 @@ impl<'a> RouteTreeSegmentData<'a> { enum_varient, }, } => { - let error_ident = static_segment_idx(*index); - let children = children.iter().map(|child| { let child = tree.get(*child).unwrap(); child.to_tokens(nests, tree, enum_name.clone(), error_enum_name.clone()) }); + if segment.is_empty() { + return quote! { + { + #(#children)* + } + }; + } + + let error_ident = static_segment_idx(*index); + quote! { { let mut segments = segments.clone(); diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index 7881bd1ef..623096dc2 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -132,7 +132,7 @@ enum Route { // UserFrame is a layout component that will receive the `user_id: String` parameter #[layout(UserFrame)] // Route1 is a non-layout component that will receive the `user_id: String` and `dynamic: String` parameters - #[route("/:dynamic?:query", Route1)] + #[route("/:dynamic?:query")] Route1 { // The type is taken from the first instance of the dynamic parameter user_id: usize, @@ -141,13 +141,13 @@ enum Route { extra: String, }, // Route2 is a non-layout component that will receive the `user_id: String` parameter - #[route("/hello_world", Route2)] + #[route("/hello_world")] // You can opt out of the layout by using the `!` prefix #[layout(!UserFrame)] Route2 { user_id: usize }, #[end_layout] #[end_nest] #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})] - #[route("/:dynamic", Route3)] + #[route("/:dynamic")] Route3 { dynamic: String }, } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 0a639ca82..478b8cc91 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -152,18 +152,10 @@ pub fn GenericLink<'a, R: Routable + Clone>(cx: Scope<'a, GenericLinkProps<'a, R }; let current_route = router.current(); - let href = current_route.to_string(); + let current_url = current_route.to_string(); + let href = target.to_string(); let ac = active_class - .and_then(|active_class| match target { - NavigationTarget::Internal(target) => { - if href == target.to_string() { - Some(format!(" {active_class}")) - } else { - None - } - } - _ => None, - }) + .and_then(|active_class| (href == current_url).then(|| format!(" {active_class}"))) .unwrap_or_default(); let id = id.unwrap_or_default(); diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index ecbf7c9fb..4ceae0fde 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -38,8 +38,6 @@ pub struct GenericRouterProps where ::Err: std::fmt::Display, { - #[props(into)] - initial_url: Option, #[props(default, into)] config: RouterCfg, } @@ -71,13 +69,6 @@ where cx.props.config.config.take().unwrap_or_default(), cx.schedule_update_any(), ); - if let Some(initial) = cx.props.initial_url.as_ref() { - router.replace( - initial - .parse() - .unwrap_or_else(|_| panic!("failed to parse initial url")), - ); - } router }); diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index d3ecf37b7..cf6a5f74a 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -51,6 +51,17 @@ pub mod prelude { pub use crate::routable::*; pub use crate::router_cfg::RouterConfiguration; pub use dioxus_router_macro::Routable; + + #[doc(hidden)] + /// A component with props used in the macro + pub trait HasProps { + /// The props type of the component. + type Props; + } + + impl

HasProps for dioxus::prelude::Component

{ + type Props = P; + } } mod utils { diff --git a/packages/router/src/navigation.rs b/packages/router/src/navigation.rs index 263059a87..ffb75eeb5 100644 --- a/packages/router/src/navigation.rs +++ b/packages/router/src/navigation.rs @@ -29,6 +29,22 @@ pub enum NavigationTarget { External(String), } +impl From<&str> for NavigationTarget +where + ::Err: Display, +{ + fn from(value: &str) -> Self { + Self::from_str(value).unwrap_or_else(|err| match err { + NavigationTargetParseError::InvalidUrl(e) => { + panic!("Failed to parse `{}` as a URL: {}", value, e) + } + NavigationTargetParseError::InvalidInternalURL(e) => { + panic!("Failed to parse `{}` as a `Routable`: {}", value, e) + } + }) + } +} + impl From for NavigationTarget { fn from(value: R) -> Self { Self::Internal(value) diff --git a/packages/router/tests/via_ssr/link.rs b/packages/router/tests/via_ssr/link.rs index afa4acf8e..88d53d923 100644 --- a/packages/router/tests/via_ssr/link.rs +++ b/packages/router/tests/via_ssr/link.rs @@ -1,47 +1,69 @@ +#![allow(non_snake_case)] + use dioxus::prelude::*; use dioxus_router::prelude::*; +use std::str::FromStr; -fn prepare(link: Component) -> String { - #![allow(non_snake_case)] - - let mut vdom = VirtualDom::new_with_props(App, AppProps { link }); +fn prepare() -> String +where + ::Err: std::fmt::Display, +{ + let mut vdom = VirtualDom::new_with_props( + App, + AppProps:: { + phantom: std::marker::PhantomData, + }, + ); let _ = vdom.rebuild(); return dioxus_ssr::render(&vdom); #[derive(Props)] - struct AppProps { - link: Component, + struct AppProps { + phantom: std::marker::PhantomData, } - impl PartialEq for AppProps { + impl PartialEq for AppProps { fn eq(&self, _other: &Self) -> bool { false } } - fn App(cx: Scope) -> Element { - use_router( - cx, - &|| RouterConfiguration { - synchronous: true, - ..Default::default() - }, - &|| Segment::content(comp(cx.props.link)), - ); - + fn App(cx: Scope>) -> Element + where + ::Err: std::fmt::Display, + { render! { h1 { "App" } - Outlet { } + GenericRouter:: { + config: RouterConfiguration { + history: Box::>::default(), + ..Default::default() + } + } } } } #[test] fn href_internal() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + #[route("/test")] + Test {}, + } + + #[inline_props] + fn Test(cx: Scope) -> Element { + todo!() + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { - target: "/test", + target: Route::Test {}, "Link" } } @@ -57,36 +79,26 @@ fn href_internal() { target = r#"target="""# ); - assert_eq!(prepare(content), expected); -} - -#[test] -fn href_named() { - fn content(cx: Scope) -> Element { - render! { - Link { - target: named::(), - "Link" - } - } - } - - let expected = format!( - "

App

Link", - href = r#"href="/""#, - default = r#"dioxus-prevent-default="onclick""#, - class = r#"class="""#, - id = r#"id="""#, - rel = r#"rel="""#, - target = r#"target="""# - ); - - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } #[test] fn href_external() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + #[route("/test")] + Test {}, + } + + #[inline_props] + fn Test(cx: Scope) -> Element { + todo!() + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { target: "https://dioxuslabs.com/", @@ -105,15 +117,29 @@ fn href_external() { target = r#"target="""# ); - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } #[test] fn with_class() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + #[route("/test")] + Test {}, + } + + #[inline_props] + fn Test(cx: Scope) -> Element { + todo!() + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { - target: "/test", + target: Route::Test {}, class: "test_class", "Link" } @@ -130,12 +156,19 @@ fn with_class() { target = r#"target="""# ); - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } #[test] fn with_active_class_active() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { target: "/", @@ -156,15 +189,29 @@ fn with_active_class_active() { target = r#"target="""# ); - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } #[test] fn with_active_class_inactive() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + #[route("/test")] + Test {}, + } + + #[inline_props] + fn Test(cx: Scope) -> Element { + todo!() + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { - target: "/test", + target: Route::Test {}, active_class: "active_class", class: "test_class", "Link" @@ -182,15 +229,29 @@ fn with_active_class_inactive() { target = r#"target="""# ); - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } #[test] fn with_id() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + #[route("/test")] + Test {}, + } + + #[inline_props] + fn Test(cx: Scope) -> Element { + todo!() + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { - target: "/test", + target: Route::Test {}, id: "test_id", "Link" } @@ -207,15 +268,29 @@ fn with_id() { target = r#"target="""# ); - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } #[test] fn with_new_tab() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + #[route("/test")] + Test {}, + } + + #[inline_props] + fn Test(cx: Scope) -> Element { + todo!() + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { - target: "/test", + target: Route::Test {}, new_tab: true, "Link" } @@ -232,12 +307,19 @@ fn with_new_tab() { target = r#"target="_blank""# ); - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } #[test] fn with_new_tab_external() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { target: "https://dioxuslabs.com/", @@ -257,15 +339,29 @@ fn with_new_tab_external() { target = r#"target="_blank""# ); - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } #[test] fn with_rel() { - fn content(cx: Scope) -> Element { + #[derive(Routable, Clone)] + enum Route { + #[route("/")] + Root {}, + #[route("/test")] + Test {}, + } + + #[inline_props] + fn Test(cx: Scope) -> Element { + todo!() + } + + #[inline_props] + fn Root(cx: Scope) -> Element { render! { Link { - target: "/test", + target: Route::Test {}, rel: "test_rel", "Link" } @@ -282,5 +378,5 @@ fn with_rel() { target = r#"target="""# ); - assert_eq!(prepare(content), expected); + assert_eq!(prepare::(), expected); } diff --git a/packages/router/tests/via_ssr/outlet.rs b/packages/router/tests/via_ssr/outlet.rs index a53ffcb11..9d83e3cf3 100644 --- a/packages/router/tests/via_ssr/outlet.rs +++ b/packages/router/tests/via_ssr/outlet.rs @@ -1,52 +1,61 @@ +#![allow(non_snake_case, unused)] + use dioxus::prelude::*; use dioxus_router::{history::MemoryHistory, prelude::*}; fn prepare(path: impl Into) -> VirtualDom { - #![allow(non_snake_case)] - let mut vdom = VirtualDom::new_with_props(App, AppProps { path: path.into() }); let _ = vdom.rebuild(); return vdom; + #[derive(Routable, Clone)] + #[rustfmt::skip] + enum Route { + #[route("/")] + RootIndex {}, + #[nest("/fixed")] + #[layout(Fixed)] + #[route("/")] + FixedIndex {}, + #[route("/fixed")] + FixedFixed {}, + #[end_layout] + #[end_nest] + #[nest("/:id")] + #[layout(Parameter)] + #[route("/")] + ParameterIndex { id: u8 }, + #[route("/fixed")] + ParameterFixed { id: u8 }, + } + #[derive(Debug, Props, PartialEq)] struct AppProps { path: String, } fn App(cx: Scope) -> Element { - use_router( - cx, - &|| RouterConfiguration { - synchronous: true, - history: Box::new(MemoryHistory::with_initial_path(cx.props.path.clone()).unwrap()), - ..Default::default() - }, - &|| { - Segment::content(comp(RootIndex)) - .fixed( - "fixed", - Route::content(comp(Fixed)).nested( - Segment::content(comp(FixedIndex)).fixed("fixed", comp(FixedFixed)), - ), - ) - .catch_all(ParameterRoute::content::(comp(Parameter)).nested( - Segment::content(comp(ParameterIndex)).fixed("fixed", comp(ParameterFixed)), - )) - }, - ); + let cfg = RouterConfiguration { + history: Box::new(MemoryHistory::with_initial_path(cx.props.path.clone()).unwrap()), + ..Default::default() + }; render! { h1 { "App" } - Outlet { } + Router { + config: cfg + } } } + #[inline_props] fn RootIndex(cx: Scope) -> Element { render! { h2 { "Root Index" } } } + #[inline_props] fn Fixed(cx: Scope) -> Element { render! { h2 { "Fixed" } @@ -54,34 +63,37 @@ fn prepare(path: impl Into) -> VirtualDom { } } + #[inline_props] fn FixedIndex(cx: Scope) -> Element { render! { h3 { "Fixed - Index" } } } + #[inline_props] fn FixedFixed(cx: Scope) -> Element { render! { h3 { "Fixed - Fixed"} } } - fn Parameter(cx: Scope) -> Element { - let val = use_route(cx)?.parameter::().unwrap(); - + #[inline_props] + fn Parameter(cx: Scope, id: u8) -> Element { render! { - h2 { "Parameter {val}" } + h2 { "Parameter {id}" } Outlet { } } } - fn ParameterIndex(cx: Scope) -> Element { + #[inline_props] + fn ParameterIndex(cx: Scope, id: u8) -> Element { render! { h3 { "Parameter - Index" } } } - fn ParameterFixed(cx: Scope) -> Element { + #[inline_props] + fn ParameterFixed(cx: Scope, id: u8) -> Element { render! { h3 { "Parameter - Fixed" } } diff --git a/packages/router/tests/web_router.rs b/packages/router/tests/web_router.rs index c402e2c70..75bff944a 100644 --- a/packages/router/tests/web_router.rs +++ b/packages/router/tests/web_router.rs @@ -6,10 +6,58 @@ use std::sync::Arc; use dioxus::prelude::*; use dioxus_router::prelude::*; use gloo::utils::document; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); +#[rustfmt::skip] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Routable)] +enum Route { + #[route("/")] + Home {}, + #[nest("/blog")] + #[route("/")] + BlogList {}, + #[route("/:id")] + BlogPost { id: usize }, +} + +fn App(cx: Scope) -> Element { + render!(Router { + config: RouterConfiguration { + history: Box::>::default(), + ..Default::default() + } + }) +} + +#[inline_props] +fn Home(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Home" } + } + }) +} + +#[inline_props] +fn BlogList(cx: Scope) -> Element { + cx.render(rsx! { + div { + + } + }) +} + +#[inline_props] +fn BlogPost(cx: Scope, id: usize) -> Element { + cx.render(rsx! { + div { } + }) +} + #[wasm_bindgen_test] fn simple_test() { fn main() { @@ -18,51 +66,6 @@ fn simple_test() { dioxus_web::launch(App); } - fn App(cx: Scope) -> Element { - use_router( - cx, - &|| RouterConfiguration { - on_update: Some(Arc::new(|_| None)), - ..Default::default() - }, - &|| { - Segment::content(comp(Home)).fixed( - "blog", - Route::empty().nested( - Segment::content(comp(BlogList)).catch_all((comp(BlogPost), PostId {})), - ), - ) - }, - ); - - render!(Outlet {}) - } - - fn Home(cx: Scope) -> Element { - cx.render(rsx! { - div { - h1 { "Home" } - } - }) - } - - fn BlogList(cx: Scope) -> Element { - cx.render(rsx! { - div { - - } - }) - } - - struct PostId; - fn BlogPost(cx: Scope) -> Element { - let _id = use_route(cx)?.parameter::().unwrap(); - - cx.render(rsx! { - div { } - }) - } - main(); let _ = document();