Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP for a Kobold router #95

Merged
merged 1 commit into from
Apr 10, 2024

Conversation

fatfingers23
Copy link
Contributor

@fatfingers23 fatfingers23 commented Apr 7, 2024

TODOS

  • First iteration of a router that can route to a view via URL on load and get parameters from the URL
  • Get params from URL. Need to find a better type to store in matchit::Router<>, probably a fn -> JsValue that can take a param
  • Moved from Arc<Mutex<Routes>> to Rc<RefCell<Routes>>
  • Remove the need to mount the router multiple times to get parameter values
  • Move to use pushstate for routing and create navigation methods

Comment on lines 165 to 158
#[component(class?: "".to_string())]
pub fn router_link(route: VString, text: VString, class: String) -> impl View + 'static {
// A router link component for using kobold_router
stateful(
|| RouteLink::new(route, text, class.into()),
|route_link: &Hook<RouteLink>| {
let onclick = move |event: MouseEvent<HtmlLinkElement>| {
navigate(route_link.route.as_str());
event.prevent_default();
};

view! {

<a class={route_link.class_names.clone()} href={route_link.route.clone()} style="text-decoration: underline;" onclick={onclick}> {&route_link.text}</a>
}
},
)
}
Copy link
Owner

@maciejhirsz maciejhirsz Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gonna start here :D

Something simple like this would work:

#[component(class?: "")]
// It's fine to call this just `link` given it's likely to be used a lot I think
pub fn link<'a>(route: &'a str, class: &'a str, children: impl View + 'a) -> impl View + 'a {
    // Only need to turn the string into an owned string here...
    let route = String::from(route);
    
    // ...so we can move it inside the closure giving `onclick`
    // a `'static` lifetime
    let onclick = move |event: MouseEvent<_>| {
        // If you have a function that takes `&str` and you have a `String`
        // or something similar, the idiomatic thing to do is to just pass
        // a simple `&String` reference, and let the compiler deref it.
        navigate(&route);
        event.prevent_default();
    };
    
    view! {
        <a {class} {onclick}>{children}</a>
    }
}

Not documented yet but since 0.10 you don't have to decorate a component with #[component(children)] to accept children, so both of these will work:

view! {
    <!link route={"/foo"}><strong>"This is a bold link to /foo"</strong></!link>

    <!link route={"/bar"} children={"This is a text link to /bar"}>
}

Ideally we should avoid doing allocations inside components (the String::from, .into() when the type is known, etc.) since those are called on every render. I have a fence for this, but it currently only works for Views, not event listeners, though that should be an easy enough addition I can help you with ("just" need to implement IntoListener<E> for Fence but there is a lot of generic types at play there). Then this function could look like this:

#[component(class?: "")]
pub fn link<'a>(route: &'a str, class: &'a str, children: impl View + 'a) -> impl View + 'a {
    let onclick = fence(route, || {
        let route = String::from(route);

        move |event: MouseEvent<_>| {
            navigate(&route);
            event.prevent_default();
        }
    });
    
    view! {
        <a {class} {onclick}>{children}</a>
    }
}

Sidenote: yes that 'a all over the signature is ugly as hell, but come Rust 2024 edition (should come october) we'll be able to remove it!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate all the details, and it was a big help! I could not get IntoListener<E> for Fence, so I took the first example you provided. However, I did add a clone to pass the route to the href attribute. I needed that attribute set for the browser's built-in styling. Was not sure if there was a better way or not.

@fatfingers23
Copy link
Contributor Author

fatfingers23 commented Apr 10, 2024

I have hit all the goals I had in the original draft post 🎉. At this point I have hit everything I can see for an initial router besides what is listed at the bottom. I think this is a decent start I think that can be built on. So I am going flip the PR to publish. Please let me know of any performance changes or suggestions you may have!

Implementation details and explanations

  • I had initially tried saving the JsValue of the views in the matchit::Router and then conditionally rendering them out. But gave up on that because I realized it was rendering each one on the first load during the add_route method when it got the JsValue from the view.
  • All routing is now handled by pushState. I also set up a custom event handler to fire off an event whenever it is called, then inside of the event handler I run whichever Fn() which is a closure to render that view
    Params from the URL are now loaded in the same pushstate event handler. It loads them into history.state, and then get_params reads from that. I was not able to personally find a way to completely get away from having get_params because I could not find a good way to pass them to the Fn() I am using in the matchit::Router
  • The route HTML should all load into a div created on the first load with the id routerView. The idea is to have eventually a <router-view> like thing found in Vue Router. Where you can have your nav bar outside of the router-view, then as you navigate the app, it just loads what is inside of that

Current improvements I see and will look into in the future

  • Unit tests
  • The fence changes discussed earlier in the PR
  • Better macro support where router_view! works like view!
  • Keeping state between routing.
  • Ability to use pushState with data that is pushed with it to send more complex data structures to routes
  • Ability to pass in a custom 404 page
  • Better documentation and a cleaner example
  • Other performance changes: I have a few clones and Strings that I think could be better handled

@fatfingers23 fatfingers23 marked this pull request as ready for review April 10, 2024 05:33
@maciejhirsz maciejhirsz merged commit 9682b8e into maciejhirsz:master Apr 10, 2024
4 checks passed
@maciejhirsz maciejhirsz mentioned this pull request Apr 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants