diff --git a/packages/yew-router/src/switch.rs b/packages/yew-router/src/switch.rs index d11d65fe41c..747ca1bc3da 100644 --- a/packages/yew-router/src/switch.rs +++ b/packages/yew-router/src/switch.rs @@ -56,7 +56,8 @@ pub enum Msg { /// A Switch that dispatches route among variants of a [`Routable`]. /// -/// When a route can't be matched, it looks for the route with `not_found` attribute. +/// When a route can't be matched, including when the path is matched but the deserialization fails, +/// it looks for the route with `not_found` attribute. /// If such a route is provided, it redirects to the specified route. /// Otherwise `html! {}` is rendered and a message is logged to console /// stating that no route can be matched. diff --git a/website/docs/concepts/router.md b/website/docs/concepts/router.md index 53d6176b0d9..bba9f674eb3 100644 --- a/website/docs/concepts/router.md +++ b/website/docs/concepts/router.md @@ -3,12 +3,20 @@ title: "Router" description: "Yew's official router" --- -[The router on crates.io](https://crates.io/crates/yew-router) +Routers in Single Page Applications (SPA) handle displaying different pages depending on what the URL is. Instead of the +default behavior of requesting a different remote resource when a link is clicked, the router instead sets the URL +locally to point to a valid route in your application. The router then detects this change and then decides what to +render. -Routers in Single Page Applications (SPA) handle displaying different pages depending on what the URL is. -Instead of the default behavior of requesting a different remote resource when a link is clicked, -the router instead sets the URL locally to point to a valid route in your application. -The router then detects this change and then decides what to render. +Yew provides router support in the `yew-router` crate. To start using it, add the dependency to your `Cargo.toml` + + + +```toml +yew-router = { git = "https://github.com/yewstack/yew.git" } +``` + +The utilities needed are provided under `yew_router::prelude`, ## Usage @@ -31,65 +39,18 @@ enum Route { } ``` -A `Route` is paired with a `` component, which finds the first variant whose path matches the -browser's current URL and passes it to the `render` callback. The callback then decides what to render. -In case no path is matched, the router navigates to the path with `not_found` attribute. If no route is specified, -nothing is rendered, and a message is logged to console stating that no route was matched. - -```rust -use yew_router::prelude::*;; -use yew::prelude::*; - -#[derive(Clone, Routable, PartialEq)] -enum Route { - #[at("/")] - Home, - #[at("/secure")] - Secure, - #[not_found] - #[at("/404")] - NotFound, -} - -#[function_component(Secure)] -fn secure() -> Html { - let history = use_history().unwrap(); - - let onclick_callback = Callback::from(move |_| history.push(Route::Home)); - html! { -
-

{ "Secure" }

- -
- } -} - -fn switch(routes: &Route) -> Html { - match routes { - Route::Home => html! {

{ "Home" }

}, - Route::Secure => html! { - - }, - Route::NotFound => html! {

{ "404" }

}, - } -} - -#[function_component(Main)] -fn app() -> Html { - html! { - render={Switch::render(switch)} /> - } -} -``` +A `Route` is paired with a `` component, which finds the variant whose path matches the browser's +current URL and passes it to the `render` callback. The callback then decides what to render. In case no path is +matched, the router navigates to the path with `not_found` attribute. If no route is specified, nothing is rendered, and +a message is logged to console stating that no route was matched. Finally, you need to register the `` component as a context. `` provides session history information to its children. -When using `yew-router` in browser environment, `` is -recommended. +When using `yew-router` in browser environment, `` is recommended. ```rust -use yew_router::prelude::*;; +use yew_router::prelude::*; use yew::prelude::*; #[derive(Clone, Routable, PartialEq)] @@ -107,11 +68,11 @@ enum Route { fn secure() -> Html { let history = use_history().unwrap(); - let onclick_callback = Callback::from(move |_| history.push(Route::Home)); + let onclick = Callback::once(move |_| history.push(Route::Home)); html! {

{ "Secure" }

- +
} } @@ -141,7 +102,8 @@ fn app() -> Html { It is also possible to extract information from a route. ```rust -# use yew_router::prelude::*; +use yew_router::prelude::*; + #[derive(Clone, Routable, PartialEq)] enum Route { #[at("/")] @@ -164,26 +126,52 @@ fn switch(routes: &Route) -> Html { } ``` -Linking to a specific post is as easy as passing the variant to `Link`: +:::note +You can have a normal `Post` variant instead of `Post {id: String}` too. For example when `Post` is rendered +with another router, the field can then be redundant as the other router is able to match and handle the path. See the +[Nested Router](#nested-router) section below for details +::: + +Note the fields must implement `Clone + PartialEq` as part of the `Route` enum. They must also implement +`std::fmt::Display` and `std::str::FromStr` for serialization and deserialization. Primitive types like integer, float, +and String already satisfy the requirements. + +In case when the form of the path matches, but the deserialization fails (as per `FromStr`). The router will consider +the route as unmatched and try to render the not found route (or a blank page if the not found route is unspecified). + +Consider this example: ```rust ,ignore - to={Route::Post { id: "new-yew-release".to_string() }}>{ "Yew v0.19 out now!" }> +#[derive(Clone, Routable, PartialEq)] +enum Route { + #[at("/news/:id")] + News { id: u8 }, + #[not_found] + #[at("/404")] + NotFound, +} +// switch function renders News and id as is. Omitted here. ``` -For more information about the route syntax and how to bind parameters, check out [route-recognizer](https://docs.rs/route-recognizer/0.3.1/route_recognizer/#routing-params). +When the segment goes over 255, `u8::from_str()` fails with `ParseIntError`, the router will then consider the route +unmatched. + +![router deserialization failure behavior](/img/router-deserialization-failure-behavior.gif) + +For more information about the route syntax and how to bind parameters, check +out [route-recognizer](https://docs.rs/route-recognizer/0.3.1/route_recognizer/#routing-params). ### History and Location -The router provides a universal `History` and `Location` struct which -can be used to access routing information. They can be retrieved by -hooks or convenient functions on `ctx.link()`. +The router provides a universal `History` and `Location` struct which can be used to access routing information. They +can be retrieved by hooks or convenient functions on `ctx.link()`. They have a couple flavours: #### `AnyHistory` and `AnyLocation` -These types are available with all routers and should be used whenever possible. -They implement a subset of `window.history` and `window.location`. +These types are available with all routers and should be used whenever possible. They implement a subset +of `window.history` and `window.location`. You can access them using the following hooks: @@ -192,27 +180,174 @@ You can access them using the following hooks: #### `BrowserHistory` and `BrowserLocation` -These are only available when `` is used. They provide -additional functionality that is not available in `AnyHistory` and +These are only available when `` is used. They provide additional functionality that is not available +in `AnyHistory` and `AnyLocation` (such as: `location.host`). ### Navigation -To navigate between pages, use either a `Link` component (which renders a `` element), the `history.push` function, or the `history.replace` function, which replaces the current page in the user's browser history instead of pushing a new one onto the stack. +`yew_router` provides a handful of tools to work with navigation. + +#### Link + +A `` renders as an `` element, the `onclick` event handler will call +[preventDefault](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault), and push the targeted page to the +history and render the desired page, which is what should be expected from a Single Page App. The default onclick of a +normal anchor element would reload the page. + +The `` element also passes its children to the `` element. Consider it a replacement of `` for in-app +routes. Except you supply a `to` attribute instead of a `href`. An example usage: + +```rust ,ignore + to={Route::Home}>{ "click here to go home" }> +``` + +Struct variants work as expected too: + +```rust ,ignore + to={Route::Post { id: "new-yew-release".to_string() }}>{ "Yew v0.19 out now!" }> +``` + +#### History API + +History API is provided for both function components and struct components. They can enable callbacks to change the +route. An `AnyHistory` instance can be obtained in either cases to manipulate the route. + +##### Function Components + +For function components, the `use_history` hook re-renders the component and returns the current route whenever the +history changes. Here's how to implement a button that navigates to the `Home` route when clicked. + +```rust ,ignore +#[function_component(MyComponent)] +pub fn my_component() -> Html { + let history = use_history().unwrap(); + let onclick = Callback::once(move |_| history.push(Route::Home)); + + html! { + <> + + + } +} +``` + +:::tip +The example here uses `Callback::once`. Use a normal callback if the target route can be the same with the route +the component is in. For example when you have a logo button on every page the that goes back to home when clicked, +clicking that button twice on home page causes the code to panic because the second click pushes an identical Home route +and won't trigger a re-render of the element. + +In other words, only use `Callback::once` when you are sure the target route is different. Or use normal callbacks only +to be safe. +::: + +If you want to replace the current history instead of pushing a new history onto the stack, use `history.replace()` +instead of `history.push()`. + +You may notice `history` has to move into the callback, so it can't be used again for other callbacks. Luckily `history` +implements `Clone`, here's for example how to have multiple buttons to different routes: + +```rust ,ignore +use yew::prelude::*; +use yew_router::prelude::*; + +#[function_component(NavItems)] +pub fn nav_items() -> Html { + let history = use_history().unwrap(); + + let go_home_button = { + let history = history.clone(); + let onclick = Callback::once(move |_| history.push(Route::Home)); + html! { + + } + }; + + let go_to_first_post_button = { + let history = history.clone(); + let onclick = Callback::once(move |_| history.push(Route::Post { id: "first-post".to_string() })); + html! { + + } + }; + + let go_to_secure_button = { + let onclick = Callback::once(move |_| history.push(Route::Secure)); + html! { + + } + }; + + html! { + <> + {go_home_button} + {go_to_first_post_button} + {go_to_secure_button} + + } +} +``` + +##### Struct Components + +For struct components, the `AnyHistory` instance can be obtained through the `ctx.link().history()` API. The rest is +identical with the function component case. Here's an example of a view function that renders a single button. + +```rust ,ignore +fn view(&self, ctx: &Context) -> Html { + let history = ctx.link().history().unwrap(); + let onclick = Callback::once(move |_| history.push(MainRoute::Home)); + html!{ + + } +} +``` + +#### Redirect + +`yew-router` also provides a `` element in the prelude. It can be used to achieve similar effects as the +history API. The element accepts a +`to` attribute as the target route. When a `` element is rendered, it internally calls `history.push()` and +changes the route. Here is an example: + +```rust ,ignore +#[function_component(SomePage)] +fn some_page() -> Html { + // made-up hook `use_user` + let user = match use_user() { + Some(user) => user, + // an early return that redirects to the login page + // technicality: `Redirect` actually renders an empty html. But since it also pushes history, the target page + // shows up immediately. Consider it a "side-effect" component. + None => return html! { + to={Route::Login}/> + }, + }; + // ... actual page content. +} +``` + +:::tip `Redirect` vs `history`, which to use +The history API is the only way to manipulate route in callbacks. +While `` can be used as return values in a component. You might also want to use `` in other +non-component context, for example in the switch function of a [Nested Router](#nested-router). +::: ### Listening to Changes -#### Functional components +#### Function Components -Simply use available hooks `use_history`, `use_location` and `use_route`. -Your components will re-render when provided values change. +Alongside the `use_history` hook, there are also `use_location` and `use_route`. Your components will re-render when +provided values change. -#### Struct components +#### Struct Components In order to react on route changes, you can pass a callback closure to the `listen()` method of `AnyHistory`. :::note -The history listener will get unregistered once it is dropped. Make sure to store the handle inside your component state. +The history listener will get unregistered once it is dropped. Make sure to store the handle inside your +component state. ::: ```rust ,ignore @@ -229,18 +364,115 @@ fn create(ctx: &Context) -> Self { } ``` +`ctx.link().location()` and `ctx.link().route::()` can also be used to retrieve the location and the route once. + ### Query Parameters #### Specifying query parameters when navigating -In order to specify query parameters when navigating to a new route, use either `history.push_with_query` or the `history.replace_with_query` functions. -It uses `serde` to serialize the parameters into query string for the URL so any type that implements `Serialize` can be passed. -In its simplest form this is just a `HashMap` containing string pairs. +In order to specify query parameters when navigating to a new route, use either `history.push_with_query` or +the `history.replace_with_query` functions. It uses `serde` to serialize the parameters into query string for the URL so +any type that implements `Serialize` can be passed. In its simplest form this is just a `HashMap` containing string +pairs. #### Obtaining query parameters for current route -`location.query` is used to obtain the query parameters. -It uses `serde` to deserialize the parameters from query string in the URL. +`location.query` is used to obtain the query parameters. It uses `serde` to deserialize the parameters from query string +in the URL. + +## Nested Router + +Nested router can be useful when the app grows larger. Consider the following router structure: + + + +![nested router diagram](/img/nested-router.gif) + +The nested `SettingsRouter` handles all urls that start with `/settings`. Additionally, it redirects urls that are not +matched to the main `NotFound` route. So `/settings/gibberish` will redirect to `/404`. + +It can be implemented with the following code: + +```rust +use yew::prelude::*; +use yew_router::prelude::*; + +#[derive(Clone, Routable, PartialEq)] +enum MainRoute { + #[at("/")] + Home, + #[at("/news")] + News, + #[at("/contact")] + Contact, + #[at("/settings/:s")] + Settings, + #[not_found] + #[at("/404")] + NotFound, +} + +#[derive(Clone, Routable, PartialEq)] +enum SettingsRoute { + #[at("/settings/profile")] + Profile, + #[at("/settings/friends")] + Friends, + #[at("/settings/theme")] + Theme, + #[not_found] + #[at("/settings/404")] + NotFound, +} + +fn switch_main(route: &MainRoute) -> Html { + match route { + MainRoute::Home => html! {

{"Home"}

}, + MainRoute::News => html! {

{"News"}

}, + MainRoute::Contact => html! {

{"Contact"}

}, + MainRoute::Settings => html! { + render={Switch::render(switch_settings)} /> + }, + MainRoute::NotFound => html! {

{"Not Found"}

}, + } +} + +fn switch_settings(route: &SettingsRoute) -> Html { + match route { + SettingsRoute::Profile => html! {

{"Profile"}

}, + SettingsRoute::Friends => html! {

{"Friends"}

}, + SettingsRoute::Theme => html! {

{"Theme"}

}, + SettingsRoute::NotFound => html! { + to={MainRoute::NotFound}/> + } + } +} + +#[function_component(App)] +pub fn app() -> Html { + html! { + + render={Switch::render(switch_main)} /> + + } +} +``` ## Relevant examples diff --git a/website/static/img/nested-router.gif b/website/static/img/nested-router.gif new file mode 100644 index 00000000000..3342335071d Binary files /dev/null and b/website/static/img/nested-router.gif differ diff --git a/website/static/img/router-deserialization-failure-behavior.gif b/website/static/img/router-deserialization-failure-behavior.gif new file mode 100644 index 00000000000..35005be058e Binary files /dev/null and b/website/static/img/router-deserialization-failure-behavior.gif differ