mirror of https://github.com/yewstack/yew
Release v0.12 of Yew Router (#1135)
* Release v0.12 of Yew Router * port router examples to web_sys * fix minimal example
This commit is contained in:
parent
bae80c0a7c
commit
ac2e6420ea
|
@ -9,10 +9,11 @@ members = [
|
|||
"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",
|
||||
"yew-router/examples/servers/warp",
|
||||
"yew-router/examples/servers/actix",
|
||||
|
||||
# Utilities
|
||||
"yewtil",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "yew-router-macro"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
@ -13,7 +13,7 @@ proc-macro = true
|
|||
[dependencies]
|
||||
syn = "1.0.2"
|
||||
quote = "1.0.1"
|
||||
yew-router-route-parser = { path = "../yew-router-route-parser" }
|
||||
yew-router-route-parser = { version = "0.12.0", path = "../yew-router-route-parser"}
|
||||
proc-macro2 = "1.0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "yew-router-route-parser"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
|
|
@ -5,22 +5,30 @@
|
|||
## ✨ **VERSION** *(DATE)*
|
||||
|
||||
- #### ⚡️ Features
|
||||
- Sample
|
||||
- None
|
||||
- #### 🛠 Fixes
|
||||
- Sample
|
||||
- None
|
||||
- #### 🚨 Breaking changes
|
||||
- Sample
|
||||
- None
|
||||
|
||||
END TEMPLATE-->
|
||||
|
||||
## ✨ **0.12.0** *(TBD)*
|
||||
|
||||
## ✨ **0.13.0** *(TBD)*
|
||||
|
||||
- #### ⚡️ Features
|
||||
- x
|
||||
- None
|
||||
- #### 🛠 Fixes
|
||||
- x
|
||||
- None
|
||||
- #### 🚨 Breaking changes
|
||||
- x
|
||||
- None
|
||||
|
||||
## ✨ **0.12.0** *2020-4-25*
|
||||
|
||||
- #### 🚨 Breaking changes
|
||||
- Bump `yew` version to `0.15`.
|
||||
- #### Extraneous
|
||||
- Remove `guide` example.
|
||||
|
||||
## ✨ **0.11.0** *2020-3-14*
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "yew-router"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>", "Sascha Grunert <mail@saschagrunert.de>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
@ -31,6 +31,10 @@ web_sys = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
yew = { version = "0.15.0", path = "../yew", features = ["services", "agent"], default-features= false, optional = true }
|
||||
yew-router-macro = { version = "0.12.0", path = "../yew-router-macro" }
|
||||
yew-router-route-parser = { version = "0.12.0", path = "../yew-router-route-parser" }
|
||||
|
||||
cfg-if = "0.1.10"
|
||||
cfg-match = "0.2"
|
||||
gloo = { version = "0.2.0", optional = true }
|
||||
|
@ -41,9 +45,6 @@ serde = { version = "1.0.104", features = ["derive"] }
|
|||
serde_json = "1.0.48"
|
||||
stdweb = { version = "0.4.20", optional = true }
|
||||
wasm-bindgen = { version = "0.2.58", optional = true }
|
||||
yew = { path = "../yew", features = ["services", "agent"], optional = true }
|
||||
yew-router-macro = { path = "../yew-router-macro" }
|
||||
yew-router-route-parser = { path = "../yew-router-route-parser" }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# Yew Router Examples
|
||||
- minimal - Demonstrates how to use this library without the use of the Router component.
|
||||
- router_component - Shows off the preferred way for how to use this library.
|
||||
|
||||
- switch - Various examples for how to construct routes with the router.
|
||||
|
||||
## Running
|
||||
Details on how to build and run these examples can be found in the readme under `servers/`.
|
||||
|
||||
Using the router in its expected use case (not fragment routing) requires that the server respond to requests for
|
||||
resources at URLs that are routes within the router with the index.html of the application.
|
|
@ -1,11 +0,0 @@
|
|||
[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"
|
|
@ -1,41 +0,0 @@
|
|||
# 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>
|
||||
}
|
||||
```
|
|
@ -1,68 +0,0 @@
|
|||
# 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>
|
||||
}
|
||||
```
|
|
@ -1,85 +0,0 @@
|
|||
# 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.
|
|
@ -1,68 +0,0 @@
|
|||
# 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.
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# 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");
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
use crate::{
|
||||
markdown_window::MarkdownWindow,
|
||||
page::{Page, PageProps},
|
||||
};
|
||||
use yew::{html::ChildrenWithProps, prelude::*, virtual_dom::VNode, Properties};
|
||||
use yew_router::{agent::RouteRequest::GetCurrentRoute, matcher::RouteMatcher, prelude::*};
|
||||
|
||||
pub struct Guide {
|
||||
router_agent: Box<dyn Bridge<RouteAgent>>,
|
||||
route: Option<Route>,
|
||||
props: GuideProps,
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone)]
|
||||
pub struct GuideProps {
|
||||
children: ChildrenWithProps<Page>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
UpdateRoute(Route),
|
||||
}
|
||||
|
||||
impl Component for Guide {
|
||||
type Message = Msg;
|
||||
type Properties = GuideProps;
|
||||
|
||||
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
let callback = link.callback(Msg::UpdateRoute);
|
||||
let router_agent = RouteAgent::bridge(callback);
|
||||
Guide {
|
||||
router_agent,
|
||||
route: None,
|
||||
props,
|
||||
}
|
||||
}
|
||||
|
||||
fn rendered(&mut self, _first_render: bool) {
|
||||
self.router_agent.send(GetCurrentRoute);
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::UpdateRoute(route) => {
|
||||
let new_route = Some(route);
|
||||
if self.route != new_route {
|
||||
self.route = new_route;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn change(&mut self, _: Self::Properties) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> VNode {
|
||||
if let Some(route) = &self.route {
|
||||
let active_markdown_uri: Option<String> = self
|
||||
.props
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
if child.props.page_url == route.to_string() {
|
||||
Some(child.props.uri)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next();
|
||||
log::debug!("active uri: {:?}", active_markdown_uri);
|
||||
|
||||
let list_items = self.props.children.iter().map(|child| {
|
||||
let x = render_page_list_item(child.props, route);
|
||||
if let yew::virtual_dom::VNode::VTag(x) = &x {
|
||||
log::debug!("{:?}", x.attributes);
|
||||
}
|
||||
x
|
||||
});
|
||||
|
||||
return html! {
|
||||
<div style="display: flex; overflow-y: hidden; height: 100%">
|
||||
<div style="min-width: 280px; border-right: 2px solid black; overflow-y: auto">
|
||||
<ul style="list-style: none; padding: 0; margin: 0">
|
||||
{for list_items}
|
||||
</ul>
|
||||
</div>
|
||||
<div style="overflow-y: auto; padding-left: 6px">
|
||||
{
|
||||
html !{
|
||||
<MarkdownWindow uri=active_markdown_uri />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
} else {
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_page_list_item(props: PageProps, route: &Route) -> Html {
|
||||
let pm: RouteMatcher = RouteMatcher::try_from(&props.page_url).unwrap();
|
||||
if pm.capture_route_into_map(&route.to_string()).is_ok() {
|
||||
log::debug!("Found an active");
|
||||
return html! {
|
||||
<li style="padding-left: 4px; padding-right: 4px; padding-top: 6px; padding-bottom: 6px; background-color: lightgray;">
|
||||
<RouterAnchor<String> route=props.page_url.clone()> {&props.title} </RouterAnchor<String>>
|
||||
</li>
|
||||
};
|
||||
} else {
|
||||
return html! {
|
||||
<li style="padding-left: 4px; padding-right: 4px; padding-top: 6px; padding-bottom: 6px; background-color: white;">
|
||||
<RouterAnchor<String> route=props.page_url.clone()> {&props.title} </RouterAnchor<String>>
|
||||
</li>
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
mod guide;
|
||||
mod markdown;
|
||||
mod markdown_window;
|
||||
mod page;
|
||||
|
||||
pub use crate::{
|
||||
guide::{Guide, GuideProps},
|
||||
page::{Page, PageProps},
|
||||
};
|
|
@ -1,62 +0,0 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
use guide::{Guide, Page};
|
||||
use yew::virtual_dom::VNode;
|
||||
|
||||
fn main() {
|
||||
yew::initialize();
|
||||
// web_logger::init();
|
||||
App::<Model>::new().mount_to_body();
|
||||
yew::run_loop();
|
||||
}
|
||||
|
||||
pub struct Model;
|
||||
|
||||
impl Component for Model {
|
||||
type Message = ();
|
||||
type Properties = ();
|
||||
|
||||
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
||||
Model
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn change(&mut self, _: Self::Properties) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> VNode {
|
||||
html! {
|
||||
<Guide>
|
||||
<Page
|
||||
uri="https://raw.githubusercontent.com/hgzimmerman/YewRouter/master/examples/guide/chapters/01_intro.md"
|
||||
page_url="/intro"
|
||||
title="Intro"
|
||||
/>
|
||||
<Page
|
||||
uri="https://raw.githubusercontent.com/hgzimmerman/YewRouter/master/examples/guide/chapters/02_router_component.md"
|
||||
page_url="/router"
|
||||
title="Router Component"
|
||||
/>
|
||||
<Page
|
||||
uri="https://raw.githubusercontent.com/hgzimmerman/YewRouter/master/examples/guide/chapters/03_route_macro.md"
|
||||
page_url="/route_macro"
|
||||
title="Route Macro"
|
||||
/>
|
||||
<Page
|
||||
uri="https://raw.githubusercontent.com/hgzimmerman/YewRouter/master/examples/guide/chapters/04_render.md"
|
||||
page_url="/render"
|
||||
title="Render"
|
||||
/>
|
||||
<Page
|
||||
uri="https://raw.githubusercontent.com/hgzimmerman/YewRouter/master/examples/guide/chapters/05_testing.md"
|
||||
page_url="/testing"
|
||||
title="Testing"
|
||||
/>
|
||||
</Guide>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
/// Original author of this code is [Nathan Ringo](https://github.com/remexre)
|
||||
/// Source: https://github.com/acmumn/mentoring/blob/master/web-client/src/view/markdown.rs
|
||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag};
|
||||
use yew::{
|
||||
html,
|
||||
virtual_dom::{VNode, VTag, VText},
|
||||
Html,
|
||||
};
|
||||
|
||||
/// Renders a string of Markdown to HTML with the default options (footnotes
|
||||
/// disabled, tables enabled).
|
||||
pub fn render_markdown(src: &str) -> Html {
|
||||
let mut elems = vec![];
|
||||
let mut spine = vec![];
|
||||
|
||||
macro_rules! add_child {
|
||||
($child:expr) => {{
|
||||
let l = spine.len();
|
||||
assert_ne!(l, 0);
|
||||
spine[l - 1].add_child($child);
|
||||
}};
|
||||
}
|
||||
|
||||
let options = Options::ENABLE_TABLES;
|
||||
|
||||
for ev in Parser::new_ext(src, options) {
|
||||
match ev {
|
||||
Event::Start(tag) => {
|
||||
spine.push(make_tag(tag));
|
||||
}
|
||||
Event::End(tag) => {
|
||||
// TODO Verify stack end.
|
||||
let l = spine.len();
|
||||
assert!(l >= 1);
|
||||
let mut top = spine.pop().unwrap();
|
||||
if let Tag::CodeBlock(_) = tag {
|
||||
let mut pre = VTag::new("pre");
|
||||
pre.add_child(top.into());
|
||||
top = pre;
|
||||
} else if let Tag::Table(aligns) = tag {
|
||||
for r in top.children.iter_mut() {
|
||||
if let VNode::VTag(ref mut vtag) = *r {
|
||||
for (i, c) in vtag.children.iter_mut().enumerate() {
|
||||
if let VNode::VTag(ref mut vtag) = *c {
|
||||
match aligns[i] {
|
||||
Alignment::None => {}
|
||||
Alignment::Left => vtag.add_class("text-left"),
|
||||
Alignment::Center => vtag.add_class("text-center"),
|
||||
Alignment::Right => vtag.add_class("text-right"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Tag::TableHead = tag {
|
||||
for c in top.children.iter_mut() {
|
||||
if let VNode::VTag(ref mut vtag) = *c {
|
||||
// TODO
|
||||
// vtag.tag = "th".into();
|
||||
vtag.add_attribute("scope", &"col");
|
||||
}
|
||||
}
|
||||
}
|
||||
if l == 1 {
|
||||
elems.push(top);
|
||||
} else {
|
||||
spine[l - 2].add_child(top.into());
|
||||
}
|
||||
}
|
||||
Event::Text(text) => add_child!(VText::new(text.to_string()).into()),
|
||||
Event::SoftBreak => add_child!(VText::new("\n".to_string()).into()),
|
||||
Event::HardBreak => add_child!(VTag::new("br").into()),
|
||||
Event::Code(code) => {
|
||||
let mut c = VTag::new("code");
|
||||
c.add_child(VText::new(code.to_string()).into());
|
||||
add_child!(c.into());
|
||||
}
|
||||
_ => println!("Unknown event: {:#?}", ev),
|
||||
}
|
||||
}
|
||||
|
||||
if elems.len() == 1 {
|
||||
VNode::VTag(Box::new(elems.pop().unwrap()))
|
||||
} else {
|
||||
html! {
|
||||
<div>{ for elems.into_iter() }</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_tag(t: Tag) -> VTag {
|
||||
match t {
|
||||
Tag::Paragraph => VTag::new("p"),
|
||||
Tag::BlockQuote => {
|
||||
let mut el = VTag::new("blockquote");
|
||||
el.add_class("blockquote");
|
||||
el
|
||||
}
|
||||
Tag::CodeBlock(lang) => {
|
||||
let mut el = VTag::new("code");
|
||||
// Different color schemes may be used for different code blocks,
|
||||
// but a different library (likely js based at the moment) would be necessary to
|
||||
// actually provide the highlighting support by locating the language
|
||||
// classes and applying dom transforms on their contents.
|
||||
match lang.as_ref() {
|
||||
"html" => el.add_class("html-language"),
|
||||
"rust" => el.add_class("rust-language"),
|
||||
"java" => el.add_class("java-language"),
|
||||
"c" => el.add_class("c-language"),
|
||||
_ => {} // Add your own language highlighting support
|
||||
};
|
||||
el
|
||||
}
|
||||
Tag::List(None) => VTag::new("ul"),
|
||||
Tag::List(Some(1)) => VTag::new("ol"),
|
||||
Tag::List(Some(ref start)) => {
|
||||
let mut el = VTag::new("ol");
|
||||
el.add_attribute("start", start);
|
||||
el
|
||||
}
|
||||
Tag::Item => VTag::new("li"),
|
||||
Tag::Table(_) => {
|
||||
let mut el = VTag::new("table");
|
||||
el.add_class("table");
|
||||
el
|
||||
}
|
||||
Tag::TableHead => VTag::new("tr"),
|
||||
Tag::TableRow => VTag::new("tr"),
|
||||
Tag::TableCell => VTag::new("td"),
|
||||
Tag::Emphasis => {
|
||||
let mut el = VTag::new("span");
|
||||
el.add_class("font-italic");
|
||||
el
|
||||
}
|
||||
Tag::Strong => {
|
||||
let mut el = VTag::new("span");
|
||||
el.add_class("font-weight-bold");
|
||||
el
|
||||
}
|
||||
Tag::Link(_lt, ref href, ref title) => {
|
||||
let mut el = VTag::new("a");
|
||||
el.add_attribute("href", href);
|
||||
if title.as_ref() != "" {
|
||||
el.add_attribute("title", title);
|
||||
}
|
||||
el
|
||||
}
|
||||
Tag::Image(_lt, ref src, ref title) => {
|
||||
let mut el = VTag::new("img");
|
||||
el.add_attribute("src", src);
|
||||
if title.as_ref() != "" {
|
||||
el.add_attribute("title", title);
|
||||
}
|
||||
el
|
||||
}
|
||||
|
||||
Tag::FootnoteDefinition(ref _footnote_id) => VTag::new("span"),
|
||||
Tag::Strikethrough => VTag::new("strike"),
|
||||
Tag::Heading(n) => {
|
||||
assert!(n > 0);
|
||||
assert!(n < 7);
|
||||
VTag::new(format!("h{}", n))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
use crate::markdown::render_markdown;
|
||||
use yew::{
|
||||
format::{Nothing, Text},
|
||||
prelude::*,
|
||||
services::{
|
||||
fetch::{FetchTask, Request, Response},
|
||||
FetchService,
|
||||
},
|
||||
virtual_dom::VNode,
|
||||
};
|
||||
|
||||
pub struct MarkdownWindow {
|
||||
fetch_service: FetchService,
|
||||
fetch_task: Option<FetchTask>,
|
||||
markdown: Option<String>,
|
||||
props: MdProps,
|
||||
link: ComponentLink<Self>,
|
||||
}
|
||||
|
||||
#[derive(Properties, Debug, Clone)]
|
||||
pub struct MdProps {
|
||||
pub uri: Option<String>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
MarkdownArrived(String),
|
||||
MarkdownFetchFailed,
|
||||
}
|
||||
|
||||
impl Component for MarkdownWindow {
|
||||
type Message = Msg;
|
||||
type Properties = MdProps;
|
||||
|
||||
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
MarkdownWindow {
|
||||
fetch_service: FetchService::new(),
|
||||
fetch_task: None,
|
||||
markdown: None,
|
||||
props,
|
||||
link,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::MarkdownArrived(md) => {
|
||||
log::info!("fetching markdown succeeded");
|
||||
self.markdown = Some(md)
|
||||
}
|
||||
Msg::MarkdownFetchFailed => log::error!("fetching markdown failed"),
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn change(&mut self, props: Self::Properties) -> bool {
|
||||
log::trace!("Change props: {:?}", props);
|
||||
self.props = props;
|
||||
self.try_fetch_markdown();
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self) -> VNode {
|
||||
if let Some(md) = &self.markdown {
|
||||
html! {
|
||||
render_markdown(md)
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MarkdownWindow {
|
||||
fn try_fetch_markdown(&mut self) {
|
||||
if let Some(uri) = &self.props.uri {
|
||||
log::info!("Getting new markdown");
|
||||
let request = Request::get(uri).body(Nothing).unwrap();
|
||||
let callback = self.link.callback(|response: Response<Text>| {
|
||||
log::info!("Got response");
|
||||
match response.body() {
|
||||
Ok(text) => Msg::MarkdownArrived(text.clone()),
|
||||
_ => Msg::MarkdownFetchFailed,
|
||||
}
|
||||
});
|
||||
self.fetch_task = self.fetch_service.fetch(request, callback).ok();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
use yew::{prelude::*, virtual_dom::VNode};
|
||||
|
||||
pub struct Page;
|
||||
|
||||
#[derive(Properties, Clone)]
|
||||
pub struct PageProps {
|
||||
pub uri: String,
|
||||
pub page_url: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl Component for Page {
|
||||
type Message = ();
|
||||
type Properties = PageProps;
|
||||
|
||||
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
||||
Page
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn change(&mut self, _: Self::Properties) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> VNode {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title> Yew Router Guide</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1" name="viewport" />
|
||||
<script>
|
||||
var Module = {};
|
||||
var __cargo_web = {};
|
||||
Object.defineProperty( Module, 'canvas', {
|
||||
get: function() {
|
||||
if( __cargo_web.canvas ) {
|
||||
return __cargo_web.canvas;
|
||||
}
|
||||
var canvas = document.createElement( 'canvas' );
|
||||
document.querySelector( 'body' ).appendChild( canvas );
|
||||
__cargo_web.canvas = canvas;
|
||||
return canvas;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; height: 100vh; overflow: hidden">
|
||||
<script>Module = {};</script> <!--This is required to avoid a runtime error at startup-->
|
||||
<script src="/guide.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -4,11 +4,13 @@ version = "0.1.0"
|
|||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../../yew" }
|
||||
yew-router = { path = "../.." }
|
||||
yew = { path = "../../../yew" }
|
||||
yew-router = { path = "../..", features = ["service", "web_sys"], default-features = false }
|
||||
web_logger = "0.2"
|
||||
log = "0.4.8"
|
||||
wee_alloc = "0.4.5"
|
||||
wasm-bindgen = "0.2.60"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Minimal Example
|
||||
Run with `cargo web start`.
|
||||
|
||||
This example shows how to use this library with only the "service" feature turned on.
|
||||
Without most of the features, you lack the `Router` component and `RouteAgent` and its associated bridges and dispatchers.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#![recursion_limit = "256"]
|
||||
use wasm_bindgen::prelude::*;
|
||||
use yew::prelude::*;
|
||||
|
||||
use yew::virtual_dom::VNode;
|
||||
|
@ -7,11 +8,10 @@ use yew_router::{route::Route, service::RouteService, Switch};
|
|||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
fn main() {
|
||||
yew::initialize();
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run_app() {
|
||||
web_logger::init();
|
||||
App::<Model>::new().mount_to_body();
|
||||
yew::run_loop();
|
||||
yew::start_app::<Model>();
|
||||
}
|
||||
|
||||
pub struct Model {
|
||||
|
@ -38,7 +38,7 @@ pub enum AppRoute {
|
|||
impl Model {
|
||||
fn change_route(&self, app_route: AppRoute) -> Callback<MouseEvent> {
|
||||
self.link.callback(move |_| {
|
||||
let route = app_route.clone(); // TODO, I don't think I should have to clone here?
|
||||
let route = app_route.clone();
|
||||
Msg::ChangeRoute(route)
|
||||
})
|
||||
}
|
|
@ -5,9 +5,13 @@ authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
|||
|
||||
edition="2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../../yew" }
|
||||
yew-router = {path = "../.." }
|
||||
yew = { path = "../../../yew"}
|
||||
yew-router = {path = "../.."}
|
||||
web_logger = "0.2"
|
||||
log = "0.4.8"
|
||||
wee_alloc = "0.4.5"
|
||||
wee_alloc = "0.4.5"
|
||||
wasm-bindgen = "0.2.60"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Router Component Example
|
||||
Run with `cargo web start`.
|
||||
|
||||
Shows how to use the `Router` component.
|
||||
This is the preferred way of how to use this library.
|
|
@ -3,6 +3,7 @@ mod a_component;
|
|||
mod b_component;
|
||||
mod c_component;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use yew::prelude::*;
|
||||
|
||||
use yew_router::{prelude::*, Switch};
|
||||
|
@ -18,11 +19,10 @@ use yew_router::switch::{AllowMissing, Permissive};
|
|||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
fn main() {
|
||||
yew::initialize();
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run_app() {
|
||||
web_logger::init();
|
||||
App::<Model>::new().mount_to_body();
|
||||
yew::run_loop();
|
||||
yew::start_app::<Model>();
|
||||
}
|
||||
|
||||
pub struct Model {}
|
|
@ -3,24 +3,22 @@
|
|||
These servers allow you to serve your application from any non-api route.
|
||||
This should prevent you from seeing 404 errors when refreshing your app after changing the route.
|
||||
|
||||
The servers rely on having the project already deployed by cargo-web, with some modifications to the output to ensure correctness.
|
||||
|
||||
## Instructions
|
||||
|
||||
Run `cargo web deploy` from the `/examples/router_component` folder to build the project that will be served by a server here.
|
||||
Then, navigate to the `/target/deploy/` directory and run:
|
||||
```sh
|
||||
sed -i 's/router_component.wasm/\/router_component.wasm/g' router_component.js
|
||||
If you don't already have wasm-pack installed, run:
|
||||
```shell script
|
||||
cargo install wasm-pack
|
||||
```
|
||||
and
|
||||
```sh
|
||||
sed -i 's/router_component.js/\/router_component.js/g' index.html
|
||||
```
|
||||
If these commands aren't ran, then the server will serve the wrong files when it tries to get a compound path.
|
||||
For example, if you request "/some/path" because it will serve `index.html`'s content, but then the request for the "router_component.js" it makes will attempt to get it from "/some/router_component.js", which doesn't exist.
|
||||
These replacements make the browser get the file from the absolute path, so it will always get the correct item.
|
||||
|
||||
Then go to your chosen server and run `cargo run` to start it.
|
||||
Change directories to your chosen example and run the following to build your wasm app.
|
||||
```shell script
|
||||
wasm-pack build --target web --out-dir ../static/ --out-name wasm
|
||||
```
|
||||
|
||||
Change directories again to one of the servers in this directory and run `cargo run`.
|
||||
They will serve files from the `static/` folder that lives in `examples/` at `localhost:8000`.
|
||||
|
||||
|
||||
## As a template
|
||||
|
||||
|
@ -30,5 +28,3 @@ You will likely have to change the assets dir constant to point elsewhere though
|
|||
const ASSETS_DIR: &str = "your/assets/dir/here";
|
||||
```
|
||||
|
||||
### Non-cargo-web
|
||||
The instructions for using `sed` and `cargo-web` above are irrelevant if you use these server templates for apps built with parcel or webpack.
|
||||
|
|
|
@ -2,7 +2,7 @@ use actix_files::NamedFile;
|
|||
use actix_web::{get, middleware, web, App, Error, HttpResponse, HttpServer};
|
||||
|
||||
// You will need to change this if you use this as a template for your application.
|
||||
const ASSETS_DIR: &str = "../../../target/deploy";
|
||||
const ASSETS_DIR: &str = "../../static";
|
||||
|
||||
#[get("/api")]
|
||||
async fn api_404() -> HttpResponse {
|
||||
|
|
|
@ -15,8 +15,7 @@ fn main() {
|
|||
let addr = (localhost, port);
|
||||
|
||||
// You will need to change this if you use this as a template for your application.
|
||||
|
||||
const ASSETS_DIR: &str = "../../../target/deploy";
|
||||
const ASSETS_DIR: &str = "../../static";
|
||||
let assets_dir: PathBuf = PathBuf::from(ASSETS_DIR);
|
||||
|
||||
let routes = api().or(static_files_handler(assets_dir));
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
*
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yew example</title>
|
||||
<script type="module">
|
||||
import init from "./wasm.js"
|
||||
init()
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
Run with `cargo run`. This example does not make use of web technologies and only demonstrates ways to construct URL matchers.
|
Loading…
Reference in New Issue