Skip to content

Commit af44076

Browse files
ranilesiku2
andauthored
Rewrite router (#1791)
* rewrite router * add support for 404 routes * support base urls * parse query params * don't use js snippets lol * cleanup code, update example * bruh fmt * test router * add more tests * wasm_test feature, CI * Add rustdocs * update docs on website * use enum for routes, add derive macro for it * fix 404 handling * fix tests * formatting * update docs, little cleanup * fix example * misc fixes * add routable macro tests * document routable macro, rustfmt * fix test, add makefile * Replace the children based API with callback based one * update example * update docs * update Cargo.toml * clippy & fmt * cleanup code * apply review * more fixes from review * fix warnings * replace function component with struct component, update docs * formatting * use `href` getter instead of reading attribute * apply review * use serde to parse query parameters * use js_sys::Array for search_params + formatting * fix doc test * Apply suggestions from code review Co-authored-by: Simon <simon@siku2.io> * Update docs/concepts/router.md apply suggestion Co-authored-by: Simon <simon@siku2.io> * apply review (part 2) * use serde for parsing query * a more modular implementation * docs for query parameters * fix doc * Apply suggestions from code review Co-authored-by: Simon <simon@siku2.io> * fixes (from review) * formatting * use new functions * not_found returns `Option<Self>`, to_route -> to_path * Apply suggestions from code review Co-authored-by: Simon <simon@siku2.io> * remove PartialEq + Clone bound * docs * fix example Co-authored-by: Simon <simon@siku2.io>
1 parent 09a41d6 commit af44076

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1097
-5277
lines changed

.github/workflows/pull-request.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ jobs:
158158
cd packages/yew
159159
wasm-pack test --chrome --firefox --headless -- --features "wasm_test"
160160
161+
- name: Run tests - yew-router
162+
run: |
163+
cd packages/yew-router
164+
wasm-pack test --chrome --firefox --headless
165+
161166
- name: Run tests - yew-functional
162167
run: |
163168
cd packages/yew-functional

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ members = [
99
# Router
1010
"packages/yew-router",
1111
"packages/yew-router-macro",
12-
"packages/yew-router-route-parser",
1312

1413
# Function components
1514
"packages/yew-functional",

docs/concepts/router.md

Lines changed: 59 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,87 +5,79 @@ description: Yew's official router
55

66
[The router on crates.io](https://crates.io/crates/yew-router)
77

8-
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.
8+
Routers in Single Page Applications (SPA) handle displaying different pages depending on what the URL is.
9+
Instead of the default behavior of requesting a different remote resource when a link is clicked,
10+
the router instead sets the URL locally to point to a valid route in your application.
11+
The router then detects this change and then decides what to render.
912

10-
## Core Elements
13+
## Usage
1114

12-
### `Route`
13-
14-
Contains a String representing everything after the domain in the url and optionally the state stored in the history API.
15-
16-
### `RouteService`
17-
18-
Communicates with the browser to get and set Routes.
19-
20-
### `RouteAgent`
21-
22-
Owns a RouteService and is used to coordinate updates when the route changes, either from within the application logic or from an event fired from the browser.
23-
24-
### `Switch`
25-
26-
The `Switch` trait is used to convert a `Route` to and from the implementer of this trait.
27-
28-
### `Router`
29-
30-
The Router component communicates with `RouteAgent` and will automatically resolve Routes it gets from the agent into switches, which it will expose via a `render` prop that allows specifying how the resulting switch gets converted to `Html`.
31-
32-
## How to use the router
33-
34-
First, you want to create a type that represents all the states of your application. Do note that while this typically is an enum, structs are supported as well, and that you can nest other items that implement `Switch` inside.
35-
36-
Then you should derive `Switch` for your type. For enums, every variant must be annotated with `
37-
#[at = "/some/route"]` (or `#[at = "/some/route"]`, but this is being phased out in favor of "at"),
38-
or if you use a struct instead, that must appear outside the struct declaration.
15+
The Router component. It takes in a callback and renders the HTML based on the returned value of the callback. It is usually placed
16+
at the top of the application.
3917

18+
Routes are defined by an `enum` which derives `Routable`:
4019
```rust
41-
#[derive(Switch)]
42-
enum AppRoute {
43-
#[at = "/login"]
44-
Login,
45-
#[at = "/register"]
46-
Register,
47-
#[at = "/delete_account"]
48-
Delete,
49-
#[at = "/posts/{id}"]
50-
ViewPost(i32),
51-
#[at = "/posts/view"]
52-
ViewPosts,
53-
#[at = "/"]
54-
Home
20+
#[derive(Routable)]
21+
enum Route {
22+
#[at("/")]
23+
Home,
24+
#[at("/secure")]
25+
Secure,
26+
#[not_found]
27+
#[at("/404")]
28+
NotFound,
5529
}
5630
```
5731

58-
:::caution
59-
Do note that the implementation generated by the derive macro for `Switch` will try to match each
60-
variant in order from first to last, so if any route could possibly match two of your specified
61-
`to` annotations, then the first one will match, and the second will never be tried. For example,
62-
if you defined the following `Switch`, the only route that would be matched would be
63-
`AppRoute::Home`.
32+
The `Router` component takes the `Routable` enum as its type parameter, finds the first variant whose path matches the
33+
browser's current URL and passes it to the `render` callback. The callback then decides what to render.
34+
In case no path is matched, the router navigates to the path with `not_found` attribute. If no route is specified,
35+
nothing is rendered, and a message is logged to console stating that no route was matched.
36+
37+
`yew_router::current_route` is used to programmatically obtain the current route.
38+
`yew_router::attach_route_listener` is used to attach a listener which is called every time route is changed.
6439

6540
```rust
66-
#[derive(Switch)]
67-
enum AppRoute {
68-
#[at = "/"]
69-
Home,
70-
#[at = "/login"]
71-
Login,
72-
#[at = "/register"]
73-
Register,
74-
#[at = "/delete_account"]
75-
Delete,
76-
#[at = "/posts/{id}"]
77-
ViewPost(i32),
78-
#[at = "/posts/view"]
79-
ViewPosts,
41+
#[function_component(Main)]
42+
fn app() -> Html {
43+
html! {
44+
<Router<Route> render=Router::render(switch) />
45+
}
46+
}
47+
48+
fn switch(route: &Route) -> Html {
49+
match route {
50+
Route::Home => html! { <h1>{ "Home" }</h1> },
51+
Route::Secure => {
52+
let callback = Callback::from(|_| yew_router::push_route(Routes::Home));
53+
html! {
54+
<div>
55+
<h1>{ "Secure" }</h1>
56+
<button onclick=callback>{ "Go Home" }</button>
57+
</div>
58+
}
59+
},
60+
Route::NotFound => html! { <h1>{ "404" }</h1> },
61+
}
8062
}
8163
```
82-
:::
8364

84-
You can also capture sections using variations of `{}` within your `#[at = ""]` annotation. `{}` means capture text until the next separator \(either "/", "?", "&", or "\#" depending on the context\). `{*}` means capture text until the following characters match, or if no characters are present, it will match anything. `{<number>}` means capture text until the specified number of separators are encountered \(example: `{2}` will capture until two separators are encountered\).
65+
### Navigation
66+
67+
To navigate between pages, use either a `Link` component (which renders a `<a>` element) or the `yew_router::push_route` function.
68+
69+
### Query Parameters
70+
71+
#### Specifying query parameters when navigating
72+
73+
In order to specify query parameters when navigating to a new route, use `yew_router::push_route_with_query` function.
74+
It uses `serde` to serialize the parameters into query string for the URL so any type that implements `Serialize` can be passed.
75+
In its simplest form this is just a `HashMap` containing string pairs.
8576

86-
For structs and enums with named fields, you must specify the field's name within the capture group like so: `{user_name}` or `{*:age}`.
77+
#### Obtaining query parameters for current route
8778

88-
The Switch trait works with capture groups that are more structured than just Strings. You can specify any type that implements `Switch`. So you can specify that the capture group is a `usize`, and if the captured section of the URL can't be converted to it, then the variant won't match.
79+
`yew_router::parse_query` is used to obtain the query parameters.
80+
It uses `serde` to deserialize the parameters from query string in the URL.
8981

9082
## Relevant examples
9183
- [Router](https://github.com/yewstack/yew/tree/master/examples/router)

examples/router/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ yew = { path = "../../packages/yew" }
1515
yew-router = { path = "../../packages/yew-router" }
1616
yewtil = { path = "../../packages/yewtil" }
1717
yew-services = { path = "../../packages/yew-services" }
18+
serde = { version = "1.0", features = ["derive"] }

examples/router/src/components/author_card.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use crate::{
2-
content::Author,
3-
generator::Generated,
4-
switch::{AppAnchor, AppRoute},
5-
};
1+
use crate::{content::Author, generator::Generated, Route};
62
use yew::prelude::*;
3+
use yew_router::prelude::*;
74

85
#[derive(Clone, Debug, PartialEq, Properties)]
96
pub struct Props {
@@ -57,9 +54,9 @@ impl Component for AuthorCard {
5754
</div>
5855
</div>
5956
<footer class="card-footer">
60-
<AppAnchor classes="card-footer-item" route=AppRoute::Author(author.seed)>
57+
<Link<Route> classes=classes!("card-footer-item") route=Route::Author { id: author.seed }>
6158
{ "Profile" }
62-
</AppAnchor>
59+
</Link<Route>>
6360
</footer>
6461
</div>
6562
}

examples/router/src/components/post_card.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use crate::{
2-
content::Post,
3-
generator::Generated,
4-
switch::{AppAnchor, AppRoute},
5-
};
1+
use crate::{content::Post, generator::Generated, Route};
62
use yew::prelude::*;
3+
use yew_router::components::Link;
74

85
#[derive(Clone, Debug, PartialEq, Properties)]
96
pub struct Props {
@@ -46,12 +43,12 @@ impl Component for PostCard {
4643
</figure>
4744
</div>
4845
<div class="card-content">
49-
<AppAnchor classes="title is-block" route=AppRoute::Post(post.seed)>
46+
<Link<Route> classes=classes!("title", "is-block") route=Route::Post { id: post.seed }>
5047
{ &post.title }
51-
</AppAnchor>
52-
<AppAnchor classes="subtitle is-block" route=AppRoute::Author(post.author.seed)>
48+
</Link<Route>>
49+
<Link<Route> classes=classes!("subtitle", "is-block") route=Route::Author { id: post.author.seed }>
5350
{ &post.author.name }
54-
</AppAnchor>
51+
</Link<Route>>
5552
</div>
5653
</div>
5754
}

examples/router/src/main.rs

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use yew::prelude::*;
2-
use yew_router::{route::Route, switch::Permissive};
2+
use yew_router::prelude::*;
33

44
mod components;
55
mod content;
@@ -9,8 +9,23 @@ use pages::{
99
author::Author, author_list::AuthorList, home::Home, page_not_found::PageNotFound, post::Post,
1010
post_list::PostList,
1111
};
12-
mod switch;
13-
use switch::{AppAnchor, AppRoute, AppRouter, PublicUrlSwitch};
12+
13+
#[derive(Routable, PartialEq, Clone, Debug)]
14+
pub enum Route {
15+
#[at("/posts/:id")]
16+
Post { id: u64 },
17+
#[at("/posts")]
18+
Posts,
19+
#[at("/authors/:id")]
20+
Author { id: u64 },
21+
#[at("/authors")]
22+
Authors,
23+
#[at("/")]
24+
Home,
25+
#[not_found]
26+
#[at("/404")]
27+
NotFound,
28+
}
1429

1530
pub enum Msg {
1631
ToggleNavbar,
@@ -50,12 +65,7 @@ impl Component for Model {
5065
{ self.view_nav() }
5166

5267
<main>
53-
<AppRouter
54-
render=AppRouter::render(Self::switch)
55-
redirect=AppRouter::redirect(|route: Route| {
56-
AppRoute::PageNotFound(Permissive(Some(route.route))).into_public()
57-
})
58-
/>
68+
<Router<Route> render=Router::render(switch) />
5969
</main>
6070
<footer class="footer">
6171
<div class="content has-text-centered">
@@ -98,22 +108,22 @@ impl Model {
98108
</div>
99109
<div class=classes!("navbar-menu", active_class)>
100110
<div class="navbar-start">
101-
<AppAnchor classes="navbar-item" route=AppRoute::Home>
111+
<Link<Route> classes=classes!("navbar-item") route=Route::Home>
102112
{ "Home" }
103-
</AppAnchor>
104-
<AppAnchor classes="navbar-item" route=AppRoute::PostList>
113+
</Link<Route>>
114+
<Link<Route> classes=classes!("navbar-item") route=Route::Posts>
105115
{ "Posts" }
106-
</AppAnchor>
116+
</Link<Route>>
107117

108118
<div class="navbar-item has-dropdown is-hoverable">
109119
<a class="navbar-link">
110120
{ "More" }
111121
</a>
112122
<div class="navbar-dropdown">
113123
<a class="navbar-item">
114-
<AppAnchor classes="navbar-item" route=AppRoute::AuthorList>
124+
<Link<Route> classes=classes!("navbar-item") route=Route::Authors>
115125
{ "Meet the authors" }
116-
</AppAnchor>
126+
</Link<Route>>
117127
</a>
118128
</div>
119129
</div>
@@ -122,30 +132,27 @@ impl Model {
122132
</nav>
123133
}
124134
}
135+
}
125136

126-
fn switch(switch: PublicUrlSwitch) -> Html {
127-
match switch.route() {
128-
AppRoute::Post(id) => {
129-
html! { <Post seed=id /> }
130-
}
131-
AppRoute::PostListPage(page) => {
132-
html! { <PostList page=page.max(1) /> }
133-
}
134-
AppRoute::PostList => {
135-
html! { <PostList page=1 /> }
136-
}
137-
AppRoute::Author(id) => {
138-
html! { <Author seed=id /> }
139-
}
140-
AppRoute::AuthorList => {
141-
html! { <AuthorList /> }
142-
}
143-
AppRoute::Home => {
144-
html! { <Home /> }
145-
}
146-
AppRoute::PageNotFound(Permissive(route)) => {
147-
html! { <PageNotFound route=route /> }
148-
}
137+
fn switch(routes: &Route) -> Html {
138+
match routes {
139+
Route::Post { id } => {
140+
html! { <Post seed=*id /> }
141+
}
142+
Route::Posts => {
143+
html! { <PostList /> }
144+
}
145+
Route::Author { id } => {
146+
html! { <Author seed=*id /> }
147+
}
148+
Route::Authors => {
149+
html! { <AuthorList /> }
150+
}
151+
Route::Home => {
152+
html! { <Home /> }
153+
}
154+
Route::NotFound => {
155+
html! { <PageNotFound /> }
149156
}
150157
}
151158
}

examples/router/src/pages/page_not_found.rs

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
11
use yew::prelude::*;
2-
use yewtil::NeqAssign;
32

4-
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
5-
pub struct Props {
6-
pub route: Option<String>,
7-
}
3+
pub struct PageNotFound;
84

9-
pub struct PageNotFound {
10-
props: Props,
11-
}
125
impl Component for PageNotFound {
136
type Message = ();
14-
type Properties = Props;
7+
type Properties = ();
158

16-
fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
17-
Self { props }
9+
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
10+
Self
1811
}
1912

2013
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
2114
unimplemented!()
2215
}
2316

24-
fn change(&mut self, props: Self::Properties) -> ShouldRender {
25-
self.props.neq_assign(props)
17+
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
18+
false
2619
}
2720

2821
fn view(&self) -> Html {
@@ -34,7 +27,7 @@ impl Component for PageNotFound {
3427
{ "Page not found" }
3528
</h1>
3629
<h2 class="subtitle">
37-
{ "This page does not seem to exist" }
30+
{ "Page page does not seem to exist" }
3831
</h2>
3932
</div>
4033
</div>

0 commit comments

Comments
 (0)