Skip to content

Commit

Permalink
Add router examples
Browse files Browse the repository at this point in the history
  • Loading branch information
jstarry committed Apr 20, 2020
1 parent 54720f2 commit 30fe5c8
Show file tree
Hide file tree
Showing 29 changed files with 1,627 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Cargo.toml
Expand Up @@ -3,9 +3,15 @@ members = [
"yew",
"yew-functional",
"yew-macro",

# Router
"yew-router",
"yew-router-macro",
"yew-router-route-parser",
"yew-router/examples/guide",
"yew-router/examples/minimal",
"yew-router/examples/router_component",
"yew-router/examples/switch",

# Examples
"examples/counter",
Expand Down
11 changes: 11 additions & 0 deletions yew-router/examples/guide/Cargo.toml
@@ -0,0 +1,11 @@
[package]
name = "guide"
version = "0.1.0"
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
edition = "2018"

[dependencies]
yew = { path = "../../../yew" }
yew-router = {path = "../../" }
log = "0.4.8"
pulldown-cmark = "0.6.1"
41 changes: 41 additions & 0 deletions yew-router/examples/guide/chapters/01_intro.md
@@ -0,0 +1,41 @@
# Intro

## What is Yew Router?
Yew Router is a router in the style of [React Router](https://reacttraining.com/react-router/web/guides/quick-start).
A router's job in the context of a frontend web application is to take part of a URL and determine what HTML to render based on that.



## Important Constructs
Yew router contains a service, an agent, routing components, and components for changing the route.
You can choose to forgo using the router itself and just use the service or agent, although the `Router` provides a higher layer of abstraction over the same domain.

#### `route!` macro
The `route!` macro allows you to specify a string that determines how the router will match part of a URL.

#### Routing Components
The `Router` and `Route` components allow you to specify what route strings to match, and what content render when they succeed.
You can tell a `Route` to render a component directly, or you can provide a closure to render an arbitrary piece of html.

#### Accessory Components
The `RouterLink` and `RouterButton` components wrap links and buttons respectively and provide ready-made components that can be used to change the route.

#### Service
The routing service interfaces directly with the browser's history API.
You can register a callback to receive messages about when routes change, or you can change the route yourself.

#### Agent
The routing agent offers a layer of orchestration to an application.
It sits between the routing components and the service, and provides an interface for you to change the route and make sure the router itself gets notified of the change.

------
### Example
This crate allows you to specify which components to render as easily as:
```rust
html! {
<Router>
<Route matcher=route!("/a/{}" CaseInsensitive) render=component::<AModel>() />
<Route matcher=route!("/c") render=component::<CModel>() />
</Router>
}
```
68 changes: 68 additions & 0 deletions yew-router/examples/guide/chapters/02_router_component.md
@@ -0,0 +1,68 @@
# Router Component

The `Router` component is used to contain `Route` components.
The `Route` components allow you to specify which routes to match, and what to render when they do.


## Logic
The `Router`, when routing, wants to find a valid target.
To do this, it will look at each of its child `Route` components.
For each `Route` component, the `Router` will attempt to match its route string against the `Route`'s matcher.
If the matcher succeeds, then a `Matches` (alias to `HashMap<&str, String>`) is produced and fed to its render function (if one is provided).
If the render function returns None, then the `Router` will continue to look an the next `Route`, but if `Some` is returned, it has completed its task and will cease looking for targets.

#### Render
If the `render` property of the `Route` component is specified, it call that function to get content to display.
The signature of this function is `fn(matches: &Matches) -> Option<Html<Router>>`.
The `Router` will only cease its search for a target if this function returns `Some`, otherwise it will continue to try other `Route`s.

The `component()` function allows you to specify a component to attempt render.
You can only call this with a type parameter of a component whose `Properties` have implemented `FromCaptures`.

Alternatively, `render()` can be called instead, which takes a closure that returns an `Option<Html<_>>`.

#### Children
If the match succeeds and the `Route` specified `children` instead of a `render` prop, the children will always be displayed.
Rendering children may be more ergonomic, but you loose access to the `&Matches` produced by the `Route`'s matcher, and as consequence you lose the ability to conditionally render

#### Both
If both a render prop and children are provided, they will both render, as long as the render function returns `Some`.
If it returns `None`, then neither will be displayed and the `Router` will continue to search for a target.

#### Neither
If neither are provided, obviously nothing will be rendered, and the search for a target will continue.

### Example
```rust
html! {
<Router>
<Route matcher=route!("/a") render=component::<AModel>() />
<Route matcher=route!("/b")>
<BModel/>
</Route>
<Route matcher=route!("/c") /> // Will never render.
<Route matcher=route!("/d") render=component::<DModel>() > // DModel will render above the EModel component.
<EModel />
</Route>
</Router>
}
```


## Ordering
Since you can create `Route`s that have matchers that can both match a given route string, you should put the more specific one above the more general one.
This ensures that the specific case has a chance to match first.

Additionally, using `{*}` or `{*:name}` in the last `Route` is a good way to provide a default case.

### Example
```rust
html! {
<Router>
<Route matcher=route!("/a/specific/path") render=component::<AModel>() />
<Route matcher=route!("/a/{}/{}") render=component::<BModel>() /> // will match any valid url that has 3 sections, and starts with `/a/` and is not `/a/specific/path`
<Route matcher=route!("/a/path/{}") render=component::<CModel>() /> // Will never match
<Route matcher=route!("{*}") render=component::<DModel>() /> // Will match anything that doesn't match above.
</Router>
}
```
85 changes: 85 additions & 0 deletions yew-router/examples/guide/chapters/03_route_macro.md
@@ -0,0 +1,85 @@
# route! macro

## Terms
* matcher string - The string provided to the `route!` macro. This string has a special syntax that defines how it matches route strings.
* route string - The section of the URL containing some combination (not necessarily all) of path query and fragment.
* matcher - The struct produced by the `route!` macro.
* path - The part of the url containing characters separated by `/` characters.
* query - The part of the url started by a `?` character, containing sections in the form `this=that`, with additional sections taking the form `&this=that`.
* fragment - The part of the url started by a `#` and can contain unstructured text.
* **any** - A section delimited by `{}` and controls capturing or skipping characters.
* **capture** - An any section that contains a alphabetical identifier within ( eg. `{capture}`). That identifier is used as the key when storing captured sections in the `Matches`.
* `Matches` - An alias to `HashMap<&str, String>`. Captured sections of the route string are stored as values, with their keys being the names that appeared in the capture section.
* **optional** - Denotes a part of the route string that does not have to match.
* **literal** - A matching route string must these sections exactly. These are made up of text as well as special characters.
* special characters - ` /?&=#`, characters that are reserved for separating sections of the route string.
* flags - extra keywords you can specify after the matcher string that determine how it will the matcher will behave.

## Description

The `route!` macro is used to define a matcher for a `Route`.
It accepts a matcher string and a few optional flags that determine how the matcher behaves.
The matcher string has a specific syntax that the macro checks at compile time to make sure that you won't encounter an error at runtime when the `Router` fails to properly parse a malformed matcher string.

You don't have to use the macro though.
You can opt to use the parser at runtime instead, or construct a vector of `MatcherTokens` yourself, although this isn't recommended.


The parser tries to ensure that these extensions to the URL produce tokens that can be used to match route strings in a predictable manner, and wont parse "improperly" formatted URLs.

Examples of URLs that the parser attempts to avoid parsing successfully include:
* Instances of double slashes (`/route/to/a//thing`)
* Empty or incomplete queries (`/route?query=` or (`/route?query`)
* Missing queries (`/route?&query=yes`)
* Path sections not starting with a slash (`im/not/really/a/route`)

To do this, the parser is made up of rules dictating where you can place any and optional sections.

### Optional
The optional section, being relatively simple in its operation, is defined mostly by where you can and cannot place them.
* The router first attempts to parse a path, then the query, then the fragment.
Optional sections are not allowed to cross these boundaries.
* To avoid the parsing double slashes in the path section, optional sections have to start with a `/` and contain either a literal, any, or a nested _optional_ if they are in a path section, and can't come immediately after a `/`, nor can a `/` come after them.
In practice, this looks like: `/a/path/to(/match)`
* Optional sections within path sections can only appear at the end of path sections.
You can't have a literal part come after an optional part.
* This means that `/a/path/(/to)(/match)` is valid.
* So is `/a/path/(/to(/match))` is also valid.
* But `/a/path(/to)/match` is not.
* Optional sections within a query can take a few forms:
* `?(query=thing)`
* `?(query=thing)(query=another_thing)`
* `(?query=thing)`
* `?query=thing(&query=another_thing)`
* `?query=thing(&query=another_thing)(&query=another_thing)`
* Optional sections for fragments are generally pretty flexible
* `(#)`
* `#(anything_you_want_here_bud)`
* `(#anything_you_want_here_bud)`

### Any
Parts delimited by `{}` can match multiple characters and will match up until the point where the parser can identify the next literal, or if one cannot be found, the end of the route string.

They can appear anywhere in paths, even between non-`/` characters like `/a/p{}ath`.
They can appear in the right hand part of queries: `/path/?query={}`.
And can be interspersed anywhere in a fragment: `#frag{}ment{}`.

* There are many types of `{}` sections.
* `{}` - This will match anything, but will be terminated by a special character `/` TODO are there other characters that can stop this matching?
* `{*}` - This will match anything and cannot be stopped by a `/`, only the next literal. This is useful for matching the whole route string. This and its named variant can appear at the beginning of the matching string.
* `{4}` - A pair of brackets containing a number will consume that many path sections before being terminated by a `/`. `{1}` is equivalent to `{}`.
* `{name}` - The named capture variant will match up to the next `/` or literal, and will add the string it captured to the `Matches` `HashMap` with the key being the specified string ("name" in this case).
* `{*:name}` - Will match anything up until the next specified literal and put the contents of its captures in the `Matches`.
* `{3:name}` - Will consume the specified number of path sections and add those contents to the `Matches`.

### Flags

* `CaseInsensitive` - This will make the literals specified in the matcher string match both the lower case and upper case variants of characters as they appear in the route string.
* `Strict` - By default, as part of an optimization step, an optional `/` is appended to the end of the path if it doesn't already have one. Setting this flag turns that off.
* `Incomplete` - By default, a route will not match unless the entire route string is matched. Enabling this flag allows the matcher to succeed as soon as all segments in the matcher are satisfied, without having to consume all of the route string.


## Note
The exact semantics and allowed syntax of the matcher string aren't fully nailed down yet.
It is likely to shift slightly over time.
If you find any inconsistencies between this document and the implementation, opening an issue in the YewRouter project would be appreciated.
68 changes: 68 additions & 0 deletions yew-router/examples/guide/chapters/04_render.md
@@ -0,0 +1,68 @@
# Render

The `render` prop in a `Route` takes a `Render` struct, which is just a wrapper around a `Fn(&Matches) -> Option<Html<Router>>`.
The reason it returns an `Option` is that it allows for rejection of routes that have captured sections that can't meet restrictions that be easily expressed in a matcher string.

### Example
```rust

html! {
<Router>
<Route path=route!("/a/{capture}") render=render(|matches: &Matches| {
let number: usize = matches["capture"].parse().ok()?;
Some(
html! {
format!("Only positive numbers allowed here: {}", number)
}
)
}) />
</Router>
}
```

## `render` function
The `render` function takes a function that implements `Fn(&Matches) -> Option<Html<Router>>`, and all it does is wrap the provided function in a `Render` struct.

### Example
```rust
let r: Render<()> = render(|_matches: &Matches| Some(html!{"Hello"}));
```

## `component` function
The `component` function is a way to create this `Render` wrapper by providing the type of the component you want to render as a type parameter.

The only caveat to being able to use the `component` function, is that the `Properties` of the specified component must implement the `FromCaptures` trait.
`FromCaptures` mandates that you implement a function called `from_matches` which has a type signature of, `Fn(&Matches) -> Option<Self>`.
Code in `component` takes the props created from this function and creates a component using them.

There exists a shortcut, though.
If you have a simple props made up only of types that implement `FromStr`, then you can derive the `FromCaptures`.


### Example
```rust
pub struct MyComponent;

#[derive(FromCaptures, Properties)]
pub struct MyComponentProps;

impl Component for MyComponent {
type Properties = MyComponentProps;
// ...
}

// ...

html! {
<Router>
<Route matcher=route!("/a/{capture}") render=component::<MyComponent>() />
</Router>
}
```

### Note
The derive functionality of `FromCaptures` is relatively basic.
It cannot handle `Option`s that you might want to populate based on optional matching sections (`()`).
It is recommended that you implement `FromCaptures` yourself for `Properties` structs that contain types that aren't automatically convertible from Strings.


34 changes: 34 additions & 0 deletions yew-router/examples/guide/chapters/05_testing.md
@@ -0,0 +1,34 @@
# Testing

To make sure that your router works reliably, you will want to test your `FromCaptures` implementations, as well as the output of your `route!` macros.


## FromCaptures
Testing implementors of is simple enough.

Just provide a `&Matches` (an alias of `HashMap<'str, String>`) to your prop's `from_matches()` method and test the expected results.

### Example
```rust

#[test]
fn creates_props() {
let mut captures: Captures = HashMap::new();
captures.insert("key", "value");
assert!(Props::from_matches(captures).is_some())
}
```

## `route!`
Testing this is often less than ideal, since you often will want to keep the macro in-line with the `Route` so you have better readability.
The best solution at the moment is to just copy + paste the `route!` macros as you see them into the tests.

### Example
```rust

#[test]
fn matcher_rejects_unexpected_route() {
let matcher = route!("/a/b");
matcher.match_path("/a/b").expect("should match");
matcher.match_path("/a/c").expect("should reject");
}

0 comments on commit 30fe5c8

Please sign in to comment.