Browse Source

update tests

Evan Almloff 2 years ago
parent
commit
35befa1784

+ 14 - 7
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<NestId>,
 }
 
@@ -38,13 +38,20 @@ impl Layout {
     pub fn parse(input: syn::parse::ParseStream, active_nests: Vec<NestId>) -> syn::Result<Self> {
         // Then parse the component name
         let _ = input.parse::<syn::Token![,]>();
-        let comp: Ident = input.parse()?;
+        let comp: Path = input.parse()?;
 
         // Then parse the props name
         let _ = input.parse::<syn::Token![,]>();
-        let props_name: Ident = input
-            .parse()
-            .unwrap_or_else(|_| format_ident!("{}Props", comp.to_string()));
+        let props_name = input.parse::<Path>().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,

+ 5 - 18
packages/router-macro/src/lib.rs

@@ -57,8 +57,6 @@ pub fn routable(input: TokenStream) -> TokenStream {
             dioxus_router::prelude::use_generic_router::<R>(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<Self, Self::Err> {
-                    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<SiteMapSegment>,

+ 21 - 46
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<Ident>,
-    props_name: Option<Ident>,
+    comp_name: Option<Path>,
+    props_name: Option<Path>,
 }
 
 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<RouteSegment>,
     pub query: Option<QuerySegment>,
@@ -73,13 +74,20 @@ impl Route {
         let route_name = variant.ident.clone();
         let args = route_attr.parse_args::<RouteArgs>()?;
         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};
-        ));
-    }
-}

+ 10 - 2
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();

+ 3 - 3
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 },
 }

+ 3 - 11
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();

+ 0 - 9
packages/router/src/components/router.rs

@@ -38,8 +38,6 @@ pub struct GenericRouterProps<R: Routable>
 where
     <R as FromStr>::Err: std::fmt::Display,
 {
-    #[props(into)]
-    initial_url: Option<String>,
     #[props(default, into)]
     config: RouterCfg<R>,
 }
@@ -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
     });
 

+ 11 - 0
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<P> HasProps for dioxus::prelude::Component<P> {
+        type Props = P;
+    }
 }
 
 mod utils {

+ 16 - 0
packages/router/src/navigation.rs

@@ -29,6 +29,22 @@ pub enum NavigationTarget<R: Routable> {
     External(String),
 }
 
+impl<R: Routable> From<&str> for NavigationTarget<R>
+where
+    <R as FromStr>::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<R: Routable> From<R> for NavigationTarget<R> {
     fn from(value: R) -> Self {
         Self::Internal(value)

+ 161 - 65
packages/router/tests/via_ssr/link.rs

@@ -1,71 +1,69 @@
+#![allow(non_snake_case)]
+
 use dioxus::prelude::*;
 use dioxus_router::prelude::*;
-
-fn prepare(link: Component) -> String {
-    #![allow(non_snake_case)]
-
-    let mut vdom = VirtualDom::new_with_props(App, AppProps { link });
+use std::str::FromStr;
+
+fn prepare<R: Routable>() -> String
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    let mut vdom = VirtualDom::new_with_props(
+        App,
+        AppProps::<R> {
+            phantom: std::marker::PhantomData,
+        },
+    );
     let _ = vdom.rebuild();
     return dioxus_ssr::render(&vdom);
 
     #[derive(Props)]
-    struct AppProps {
-        link: Component,
+    struct AppProps<R: Routable> {
+        phantom: std::marker::PhantomData<R>,
     }
 
-    impl PartialEq for AppProps {
+    impl<R: Routable> PartialEq for AppProps<R> {
         fn eq(&self, _other: &Self) -> bool {
             false
         }
     }
 
-    fn App(cx: Scope<AppProps>) -> Element {
-        use_router(
-            cx,
-            &|| RouterConfiguration {
-                synchronous: true,
-                ..Default::default()
-            },
-            &|| Segment::content(comp(cx.props.link)),
-        );
-
+    fn App<R: Routable>(cx: Scope<AppProps<R>>) -> Element
+    where
+        <R as FromStr>::Err: std::fmt::Display,
+    {
         render! {
             h1 { "App" }
-            Outlet { }
+            GenericRouter::<R> {
+                config: RouterConfiguration {
+                    history: Box::<MemoryHistory::<R>>::default(),
+                    ..Default::default()
+                }
+            }
         }
     }
 }
 
 #[test]
 fn href_internal() {
-    fn content(cx: Scope) -> Element {
-        render! {
-            Link {
-                target: "/test",
-                "Link"
-            }
-        }
+    #[derive(Routable, Clone)]
+    enum Route {
+        #[route("/")]
+        Root {},
+        #[route("/test")]
+        Test {},
     }
 
-    let expected = format!(
-        "<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
-        href = r#"href="/test""#,
-        default = r#"dioxus-prevent-default="onclick""#,
-        class = r#"class="""#,
-        id = r#"id="""#,
-        rel = r#"rel="""#,
-        target = r#"target="""#
-    );
-
-    assert_eq!(prepare(content), expected);
-}
+    #[inline_props]
+    fn Test(cx: Scope) -> Element {
+        todo!()
+    }
 
-#[test]
-fn href_named() {
-    fn content(cx: Scope) -> Element {
+    #[inline_props]
+    fn Root(cx: Scope) -> Element {
         render! {
             Link {
-                target: named::<RootIndex>(),
+                target: Route::Test {},
                 "Link"
             }
         }
@@ -73,7 +71,7 @@ fn href_named() {
 
     let expected = format!(
         "<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
-        href = r#"href="/""#,
+        href = r#"href="/test""#,
         default = r#"dioxus-prevent-default="onclick""#,
         class = r#"class="""#,
         id = r#"id="""#,
@@ -81,12 +79,26 @@ fn href_named() {
         target = r#"target="""#
     );
 
-    assert_eq!(prepare(content), expected);
+    assert_eq!(prepare::<Route>(), 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::<Route>(), 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::<Route>(), 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::<Route>(), 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::<Route>(), 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::<Route>(), 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::<Route>(), 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::<Route>(), 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::<Route>(), expected);
 }

+ 41 - 29
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<String>) -> 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<AppProps>) -> 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::<u8>(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<String>) -> 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::<u8>().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" }
         }

+ 48 - 45
packages/router/tests/web_router.rs

@@ -6,61 +6,64 @@ 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);
 
-#[wasm_bindgen_test]
-fn simple_test() {
-    fn main() {
-        console_error_panic_hook::set_once();
-        wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
-        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 {})),
-                    ),
-                )
-            },
-        );
+#[rustfmt::skip]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Routable)]
+enum Route {
+    #[route("/")]
+    Home {},
+    #[nest("/blog")]
+        #[route("/")]
+        BlogList {},
+        #[route("/:id")]
+        BlogPost { id: usize },
+}
 
-        render!(Outlet {})
-    }
+fn App(cx: Scope) -> Element {
+    render!(Router {
+        config: RouterConfiguration {
+            history: Box::<WebHistory<Route>>::default(),
+            ..Default::default()
+        }
+    })
+}
 
-    fn Home(cx: Scope) -> Element {
-        cx.render(rsx! {
-            div {
-                h1 { "Home" }
-            }
-        })
-    }
+#[inline_props]
+fn Home(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            h1 { "Home" }
+        }
+    })
+}
 
-    fn BlogList(cx: Scope) -> Element {
-        cx.render(rsx! {
-            div {
+#[inline_props]
+fn BlogList(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
 
-            }
-        })
-    }
+        }
+    })
+}
 
-    struct PostId;
-    fn BlogPost(cx: Scope) -> Element {
-        let _id = use_route(cx)?.parameter::<PostId>().unwrap();
+#[inline_props]
+fn BlogPost(cx: Scope, id: usize) -> Element {
+    cx.render(rsx! {
+        div { }
+    })
+}
 
-        cx.render(rsx! {
-            div { }
-        })
+#[wasm_bindgen_test]
+fn simple_test() {
+    fn main() {
+        console_error_panic_hook::set_once();
+        wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
+        dioxus_web::launch(App);
     }
 
     main();