Update Yew Router as per #2113 (#2118)

* Add Redirect Comp.

* Fix router behaviour.

* Fix output.

* Fix pr-flow.

* Remove Redirect.

* Readd 77b46bf.

* Router Context, History and Location.

* Finish Porting.

* Add Switch, Fix example.

* Add state.

* Fix pr-flow.

* Fix pr-flow.

* Fix unstable feature for 1.49.

* Add documentation.

* Error Types & Simplify Implementation.

* Add some tests.

* Update documentation.

* Fix route outside of a Switch.
This commit is contained in:
Kaede Hoshikawa 2021-11-11 20:08:42 +09:00 committed by GitHub
parent 6edb135a4d
commit f43760e3ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1456 additions and 401 deletions

View File

@ -46,7 +46,7 @@ impl Component for AuthorCard {
</div>
</div>
<footer class="card-footer">
<Link<Route> classes={classes!("card-footer-item")} route={Route::Author { id: author.seed }}>
<Link<Route> classes={classes!("card-footer-item")} to={Route::Author { id: author.seed }}>
{ "Profile" }
</Link<Route>>
</footer>

View File

@ -34,10 +34,10 @@ impl Component for PostCard {
</figure>
</div>
<div class="card-content">
<Link<Route> classes={classes!("title", "is-block")} route={Route::Post { id: post.seed }}>
<Link<Route> classes={classes!("title", "is-block")} to={Route::Post { id: post.seed }}>
{ &post.title }
</Link<Route>>
<Link<Route> classes={classes!("subtitle", "is-block")} route={Route::Author { id: post.author.seed }}>
<Link<Route> classes={classes!("subtitle", "is-block")} to={Route::Author { id: post.author.seed }}>
{ &post.author.name }
</Link<Route>>
</div>

View File

@ -56,11 +56,11 @@ impl Component for Model {
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
<BrowserRouter>
{ self.view_nav(ctx.link()) }
<main>
<Router<Route> render={Router::render(switch)} />
<Switch<Route> render={Switch::render(switch)} />
</main>
<footer class="footer">
<div class="content has-text-centered">
@ -72,7 +72,7 @@ impl Component for Model {
<a href="https://unsplash.com">{ "Unsplash" }</a>
</div>
</footer>
</>
</BrowserRouter>
}
}
}
@ -99,10 +99,10 @@ impl Model {
</div>
<div class={classes!("navbar-menu", active_class)}>
<div class="navbar-start">
<Link<Route> classes={classes!("navbar-item")} route={Route::Home}>
<Link<Route> classes={classes!("navbar-item")} to={Route::Home}>
{ "Home" }
</Link<Route>>
<Link<Route> classes={classes!("navbar-item")} route={Route::Posts}>
<Link<Route> classes={classes!("navbar-item")} to={Route::Posts}>
{ "Posts" }
</Link<Route>>
@ -112,7 +112,7 @@ impl Model {
</a>
<div class="navbar-dropdown">
<a class="navbar-item">
<Link<Route> classes={classes!("navbar-item")} route={Route::Authors}>
<Link<Route> classes={classes!("navbar-item")} to={Route::Authors}>
{ "Meet the authors" }
</Link<Route>>
</a>

View File

@ -46,7 +46,7 @@ impl Component for Post {
</h1>
<h2 class="subtitle">
{ "by " }
<Link<Route> classes={classes!("has-text-weight-semibold")} route={Route::Author { id: post.meta.author.seed }}>
<Link<Route> classes={classes!("has-text-weight-semibold")} to={Route::Author { id: post.meta.author.seed }}>
{ &post.meta.author.name }
</Link<Route>>
</h2>
@ -74,7 +74,7 @@ impl Post {
</figure>
<div class="media-content">
<div class="content">
<Link<Route> classes={classes!("is-size-5")} route={Route::Author { id: quote.author.seed }}>
<Link<Route> classes={classes!("is-size-5")} to={Route::Author { id: quote.author.seed }}>
<strong>{ &quote.author.name }</strong>
</Link<Route>>
<p class="is-family-secondary">

View File

@ -2,6 +2,7 @@ use crate::components::{pagination::Pagination, post_card::PostCard};
use crate::Route;
use serde::{Deserialize, Serialize};
use yew::prelude::*;
use yew_router::prelude::*;
const ITEMS_PER_PAGE: u64 = 10;
const TOTAL_PAGES: u64 = u64::MAX / ITEMS_PER_PAGE;
@ -25,23 +26,27 @@ impl Component for PostList {
Self
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::ShowPage(page) => {
yew_router::push_route_with_query(Route::Posts, PageQuery { page }).unwrap();
ctx.link()
.history()
.unwrap()
.push_with_query(Route::Posts, PageQuery { page })
.unwrap();
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let page = self.current_page();
let page = self.current_page(ctx);
html! {
<div class="section container">
<h1 class="title">{ "Posts" }</h1>
<h2 class="subtitle">{ "All of our quality writing in one place" }</h2>
{ self.view_posts() }
{ self.view_posts(ctx) }
<Pagination
{page}
total_pages={TOTAL_PAGES}
@ -52,8 +57,8 @@ impl Component for PostList {
}
}
impl PostList {
fn view_posts(&self) -> Html {
let start_seed = (self.current_page() - 1) * ITEMS_PER_PAGE;
fn view_posts(&self, ctx: &Context<Self>) -> Html {
let start_seed = (self.current_page(ctx) - 1) * ITEMS_PER_PAGE;
let mut cards = (0..ITEMS_PER_PAGE).map(|seed_offset| {
html! {
<li class="list-item mb-5">
@ -77,9 +82,9 @@ impl PostList {
}
}
fn current_page(&self) -> u64 {
yew_router::parse_query::<PageQuery>()
.map(|it| it.page)
.unwrap_or(1)
fn current_page(&self, ctx: &Context<Self>) -> u64 {
let location = ctx.link().location().unwrap();
location.query::<PageQuery>().map(|it| it.page).unwrap_or(1)
}
}

View File

@ -189,23 +189,25 @@ pub fn routable_derive_impl(input: Routable) -> TokenStream {
let from_path = input.build_from_path();
let to_path = input.build_to_path();
let not_found_route = match not_found_route {
let maybe_not_found_route = match not_found_route {
Some(route) => quote! { ::std::option::Option::Some(Self::#route) },
None => quote! { ::std::option::Option::None },
};
let cache_thread_local_ident = Ident::new(
&format!("__{}_ROUTER_CURRENT_ROUTE_CACHE", ident),
ident.span(),
);
let maybe_default = match not_found_route {
Some(route) => {
quote! {
impl ::std::default::Default for #ident {
fn default() -> Self {
Self::#route
}
}
}
}
None => TokenStream::new(),
};
quote! {
::std::thread_local! {
#[doc(hidden)]
#[allow(non_upper_case_globals)]
static #cache_thread_local_ident: ::std::cell::RefCell<::std::option::Option<#ident>> = ::std::cell::RefCell::new(::std::option::Option::None);
}
#[automatically_derived]
impl ::yew_router::Routable for #ident {
#from_path
@ -216,32 +218,17 @@ pub fn routable_derive_impl(input: Routable) -> TokenStream {
}
fn not_found_route() -> ::std::option::Option<Self> {
#not_found_route
}
fn current_route() -> ::std::option::Option<Self> {
#cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
#maybe_not_found_route
}
fn recognize(pathname: &str) -> ::std::option::Option<Self> {
::std::thread_local! {
static ROUTER: ::yew_router::__macro::Router = ::yew_router::__macro::build_router::<#ident>();
}
let route = ROUTER.with(|router| ::yew_router::__macro::recognize_with_router(router, pathname));
{
let route = ::std::clone::Clone::clone(&route);
#cache_thread_local_ident.with(move |val| {
*val.borrow_mut() = route;
});
}
route
}
fn cleanup() {
#cache_thread_local_ident.with(move |val| {
*val.borrow_mut() = ::std::option::Option::None;
});
ROUTER.with(|router| ::yew_router::__macro::recognize_with_router(router, pathname))
}
}
#maybe_default
}
}

View File

@ -19,10 +19,12 @@ yew-router-macro = { path = "../yew-router-macro" }
wasm-bindgen = "0.2"
js-sys = "0.3"
gloo = "0.3"
gloo = { version = "0.3", features = ["futures"] }
route-recognizer = "0.3"
serde = "1"
serde_urlencoded = "0.7"
serde-wasm-bindgen = "0.3.1"
thiserror = "1.0.30"
[dependencies.web-sys]
version = "0.3"

View File

@ -1,20 +1,25 @@
use crate::{service, Routable};
use std::marker::PhantomData;
use wasm_bindgen::UnwrapThrowExt;
use yew::prelude::*;
use crate::history::History;
use crate::scope_ext::RouterScopeExt;
use crate::Routable;
/// Props for [`Link`]
#[derive(Properties, Clone, PartialEq)]
pub struct LinkProps<R: Routable + Clone + PartialEq> {
pub struct LinkProps<R: Routable> {
/// CSS classes to add to the anchor element (optional).
#[prop_or_default]
pub classes: Classes,
/// Route that will be pushed when the anchor is clicked.
pub route: R,
pub to: R,
pub children: Children,
}
/// A wrapper around `<a>` tag to be used with [`Router`](crate::Router)
pub struct Link<R: Routable + Clone + PartialEq + 'static> {
pub struct Link<R: Routable + 'static> {
_data: PhantomData<R>,
}
@ -22,7 +27,7 @@ pub enum Msg {
OnClick,
}
impl<R: Routable + Clone + PartialEq + 'static> Component for Link<R> {
impl<R: Routable + 'static> Component for Link<R> {
type Message = Msg;
type Properties = LinkProps<R>;
@ -33,7 +38,10 @@ impl<R: Routable + Clone + PartialEq + 'static> Component for Link<R> {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::OnClick => {
service::push_route(ctx.props().route.clone());
ctx.link()
.history()
.expect_throw("failed to read history")
.push(ctx.props().to.clone());
false
}
}
@ -42,7 +50,7 @@ impl<R: Routable + Clone + PartialEq + 'static> Component for Link<R> {
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<a class={ctx.props().classes.clone()}
href={ctx.props().route.to_path()}
href={ctx.props().to.to_path()}
onclick={ctx.link().callback(|e: MouseEvent| {
e.prevent_default();
Msg::OnClick

View File

@ -1,4 +1,6 @@
//! Components to interface with [Router][crate::Router].
mod link;
mod redirect;
pub use link::*;
pub use redirect::*;

View File

@ -0,0 +1,31 @@
use wasm_bindgen::UnwrapThrowExt;
use yew::prelude::*;
use crate::history::History;
use crate::hooks::use_history;
use crate::Routable;
/// Props for [`Redirect`]
#[derive(Properties, Clone, PartialEq)]
pub struct RedirectProps<R: Routable> {
/// Route that will be pushed when the component is rendered.
pub to: R,
}
/// A component that will redirect to specified route when rendered.
#[function_component(Redirect)]
pub fn redirect<R>(props: &RedirectProps<R>) -> Html
where
R: Routable + 'static,
{
let history = use_history().expect_throw("failed to read history.");
let target_route = props.to.clone();
use_effect(move || {
history.push(target_route.clone());
|| {}
});
Html::default()
}

View File

@ -0,0 +1,684 @@
//! A module that provides universal session history and location information.
use std::borrow::Cow;
use std::cell::RefCell;
use std::rc::{Rc, Weak};
use gloo::events::EventListener;
use serde::de::DeserializeOwned;
use serde::Serialize;
use thiserror::Error;
use wasm_bindgen::{JsValue, UnwrapThrowExt};
use yew::callback::Callback;
use yew::utils::window;
use crate::utils::base_url;
use crate::Routable;
/// A History Listener to manage callbacks registered on a [`History`].
///
/// This Listener has the same behaviour as the [`EventListener`] from [`gloo`]
/// that the underlying callback will be unregistered when the listener is dropped.
pub struct HistoryListener {
_listener: Rc<Callback<()>>,
}
#[derive(Error, Debug)]
pub enum HistoryError {
#[error("failed to serialize / deserialize state.")]
State(#[from] serde_wasm_bindgen::Error),
#[error("failed to serialize query.")]
QuerySer(#[from] serde_urlencoded::ser::Error),
#[error("failed to deserialize query.")]
QueryDe(#[from] serde_urlencoded::de::Error),
}
pub type HistoryResult<T> = std::result::Result<T, HistoryError>;
/// A trait to provide [`History`] access.
pub trait History: Clone + PartialEq {
type Location: Location<History = Self> + 'static;
/// Returns the number of elements in [`History`].
fn len(&self) -> usize;
/// Returns true if the current [`History`] is empty.
fn is_empty(&self) -> bool {
self.len() == 0
}
/// Moves back 1 page in [`History`].
fn back(&self) {
self.go(-1);
}
/// Moves forward 1 page in [`History`].
fn forward(&self) {
self.go(1);
}
/// Loads a specific page in [`History`] with a `delta` relative to current page.
///
/// See: <https://developer.mozilla.org/en-US/docs/Web/API/History/go>
fn go(&self, delta: isize);
/// Pushes a [`Routable`] entry with [`None`] being the state.
fn push(&self, route: impl Routable);
/// Replaces the current history entry with provided [`Routable`] and [`None`] state.
fn replace(&self, route: impl Routable);
/// Pushes a [`Routable`] entry with state.
///
/// The implementation of state serialization differs between [`History`] types.
///
/// For [`BrowserHistory`], it uses [`serde_wasm_bindgen`] where as other types uses
/// [`Any`](std::any::Any).
fn push_with_state<T>(&self, route: impl Routable, state: T) -> HistoryResult<()>
where
T: Serialize + 'static;
/// Replaces the current history entry with provided [`Routable`] and state.
///
/// The implementation of state serialization differs between [`History`] types.
///
/// For [`BrowserHistory`], it uses [`serde_wasm_bindgen`] where as other types uses
/// [`Any`](std::any::Any).
fn replace_with_state<T>(&self, route: impl Routable, state: T) -> HistoryResult<()>
where
T: Serialize + 'static;
/// Same as `.push()` but affix the queries to the end of the route.
fn push_with_query<Q>(&self, route: impl Routable, query: Q) -> HistoryResult<()>
where
Q: Serialize;
/// Same as `.replace()` but affix the queries to the end of the route.
fn replace_with_query<Q>(&self, route: impl Routable, query: Q) -> HistoryResult<()>
where
Q: Serialize;
/// Same as `.push_with_state()` but affix the queries to the end of the route.
fn push_with_query_and_state<Q, T>(
&self,
route: impl Routable,
query: Q,
state: T,
) -> HistoryResult<()>
where
Q: Serialize,
T: Serialize + 'static;
/// Same as `.replace_with_state()` but affix the queries to the end of the route.
fn replace_with_query_and_state<Q, T>(
&self,
route: impl Routable,
query: Q,
state: T,
) -> HistoryResult<()>
where
Q: Serialize,
T: Serialize + 'static;
/// Creates a Listener that will be notified when current state changes.
///
/// This method returns a [`HistoryListener`] that will automatically unregister the callback
/// when dropped.
fn listen<CB>(&self, callback: CB) -> HistoryListener
where
CB: Fn() + 'static;
/// Returns the associated [`Location`] of the current history.
fn location(&self) -> Self::Location;
fn into_any_history(self) -> AnyHistory;
/// Returns the State.
///
/// The implementation differs between [`History`] type.
///
/// For [`BrowserHistory`], it uses [`serde_wasm_bindgen`] where as other types uses
/// `downcast_ref()` on [`Any`](std::any::Any).
fn state<T>(&self) -> HistoryResult<T>
where
T: DeserializeOwned + 'static;
}
/// A [`History`] that is implemented with [`web_sys::History`] that provides native browser
/// history and state access.
#[derive(Clone)]
pub struct BrowserHistory {
inner: web_sys::History,
callbacks: Rc<RefCell<Vec<Weak<Callback<()>>>>>,
}
impl PartialEq for BrowserHistory {
fn eq(&self, _rhs: &Self) -> bool {
// All browser histories are created equal.
true
}
}
impl History for BrowserHistory {
type Location = BrowserLocation;
fn len(&self) -> usize {
self.inner.length().expect_throw("failed to get length.") as usize
}
fn go(&self, delta: isize) {
self.inner
.go_with_delta(delta as i32)
.expect_throw("failed to call go.")
}
fn push(&self, route: impl Routable) {
let url = Self::route_to_url(route);
self.inner
.push_state_with_url(&JsValue::NULL, "", Some(&url))
.expect("failed to push state.");
self.notify_callbacks();
}
fn replace(&self, route: impl Routable) {
let url = Self::route_to_url(route);
self.inner
.replace_state_with_url(&JsValue::NULL, "", Some(&url))
.expect("failed to replace history.");
self.notify_callbacks();
}
fn push_with_state<T>(&self, route: impl Routable, state: T) -> HistoryResult<()>
where
T: Serialize + 'static,
{
let url = Self::route_to_url(route);
let state = serde_wasm_bindgen::to_value(&state)?;
self.inner
.push_state_with_url(&state, "", Some(&url))
.expect("failed to push state.");
self.notify_callbacks();
Ok(())
}
fn replace_with_state<T>(&self, route: impl Routable, state: T) -> HistoryResult<()>
where
T: Serialize + 'static,
{
let url = Self::route_to_url(route);
let state = serde_wasm_bindgen::to_value(&state)?;
self.inner
.replace_state_with_url(&state, "", Some(&url))
.expect("failed to replace state.");
self.notify_callbacks();
Ok(())
}
fn push_with_query<Q>(&self, route: impl Routable, query: Q) -> HistoryResult<()>
where
Q: Serialize,
{
let url = Self::route_to_url(route);
let query = serde_urlencoded::to_string(query)?;
self.inner
.push_state_with_url(&JsValue::NULL, "", Some(&format!("{}?{}", url, query)))
.expect("failed to push history.");
self.notify_callbacks();
Ok(())
}
fn replace_with_query<Q>(&self, route: impl Routable, query: Q) -> HistoryResult<()>
where
Q: Serialize,
{
let url = Self::route_to_url(route);
let query = serde_urlencoded::to_string(query)?;
self.inner
.replace_state_with_url(&JsValue::NULL, "", Some(&format!("{}?{}", url, query)))
.expect("failed to replace history.");
self.notify_callbacks();
Ok(())
}
fn push_with_query_and_state<Q, T>(
&self,
route: impl Routable,
query: Q,
state: T,
) -> HistoryResult<()>
where
Q: Serialize,
T: Serialize + 'static,
{
let url = Self::route_to_url(route);
let query = serde_urlencoded::to_string(query)?;
let state = serde_wasm_bindgen::to_value(&state)?;
self.inner
.push_state_with_url(&state, "", Some(&format!("{}?{}", url, query)))
.expect("failed to push history.");
self.notify_callbacks();
Ok(())
}
fn replace_with_query_and_state<Q, T>(
&self,
route: impl Routable,
query: Q,
state: T,
) -> HistoryResult<()>
where
Q: Serialize,
T: Serialize + 'static,
{
let url = Self::route_to_url(route);
let query = serde_urlencoded::to_string(query)?;
let state = serde_wasm_bindgen::to_value(&state)?;
self.inner
.replace_state_with_url(&state, "", Some(&format!("{}?{}", url, query)))
.expect("failed to replace history.");
self.notify_callbacks();
Ok(())
}
fn listen<CB>(&self, callback: CB) -> HistoryListener
where
CB: Fn() + 'static,
{
// Callbacks do not receive a copy of [`History`] to prevent reference cycle.
let cb = Rc::new(Callback::from(move |_| callback()));
self.callbacks.borrow_mut().push(Rc::downgrade(&cb));
HistoryListener { _listener: cb }
}
fn location(&self) -> Self::Location {
BrowserLocation::new(self.clone())
}
fn into_any_history(self) -> AnyHistory {
AnyHistory::Browser(self)
}
fn state<T>(&self) -> HistoryResult<T>
where
T: DeserializeOwned + 'static,
{
serde_wasm_bindgen::from_value(self.inner.state().expect_throw("failed to read state."))
.map_err(|e| e.into())
}
}
impl Default for BrowserHistory {
fn default() -> Self {
// We create browser history only once.
thread_local! {
static BROWSER_HISTORY: RefCell<Option<BrowserHistory>> = RefCell::default();
static LISTENER: RefCell<Option<EventListener>> = RefCell::default();
}
BROWSER_HISTORY.with(|m| {
let mut m = m.borrow_mut();
let history = match *m {
Some(ref m) => m.clone(),
None => {
let window = window();
let inner = window
.history()
.expect_throw("Failed to create browser history. Are you using a browser?");
let callbacks = Rc::default();
let history = Self { inner, callbacks };
let history_clone = history.clone();
// Listens to popstate.
LISTENER.with(move |m| {
let mut listener = m.borrow_mut();
*listener = Some(EventListener::new(&window, "popstate", move |_| {
history_clone.notify_callbacks();
}));
});
history
}
};
*m = Some(history.clone());
history
})
}
}
impl BrowserHistory {
/// Creates a new [`BrowserHistory`]
pub fn new() -> Self {
Self::default()
}
fn route_to_url(route: impl Routable) -> Cow<'static, str> {
let base = base_url();
let url = route.to_path();
let path = match base {
Some(base) => {
let path = format!("{}{}", base, url);
if path.is_empty() {
Cow::from("/")
} else {
path.into()
}
}
None => url.into(),
};
path
}
fn notify_callbacks(&self) {
let callables = {
let mut callbacks_ref = self.callbacks.borrow_mut();
// Any gone weak references are removed when called.
let (callbacks, callbacks_weak) = callbacks_ref.iter().cloned().fold(
(Vec::new(), Vec::new()),
|(mut callbacks, mut callbacks_weak), m| {
if let Some(m_strong) = m.clone().upgrade() {
callbacks.push(m_strong);
callbacks_weak.push(m);
}
(callbacks, callbacks_weak)
},
);
*callbacks_ref = callbacks_weak;
callbacks
};
for callback in callables {
callback.emit(())
}
}
}
/// A trait to to provide [`Location`] information.
pub trait Location: Clone + PartialEq {
type History: History<Location = Self> + 'static;
/// Returns the `pathname` on the [`Location`] struct.
fn pathname(&self) -> String;
/// Returns the queries of current URL in [`String`]
fn search(&self) -> String;
/// Returns the queries of current URL parsed as `T`.
fn query<T>(&self) -> HistoryResult<T>
where
T: DeserializeOwned;
/// Returns the hash fragment of current URL.
fn hash(&self) -> String;
/// Returns current route or `None` if none matched.
fn route<R>(&self) -> Option<R>
where
R: Routable;
}
/// The [`Location`] type for [`BrowserHistory`].
///
/// Most functionality of this type is provided by [`web_sys::Location`].
///
/// This type also provides additional methods that are unique to Browsers and are not available in [`Location`].
///
/// This types is read-only as most setters on `window.location` would cause a reload.
#[derive(Clone)]
pub struct BrowserLocation {
inner: web_sys::Location,
_history: BrowserHistory,
}
impl PartialEq for BrowserLocation {
fn eq(&self, rhs: &Self) -> bool {
self._history == rhs._history
}
}
impl Location for BrowserLocation {
type History = BrowserHistory;
fn pathname(&self) -> String {
self.inner
.pathname()
.expect_throw("failed to get pathname.")
}
fn search(&self) -> String {
self.inner.search().expect_throw("failed to get search.")
}
fn query<T>(&self) -> HistoryResult<T>
where
T: DeserializeOwned,
{
let query = self.search();
serde_urlencoded::from_str(query.strip_prefix('?').unwrap_or("")).map_err(|e| e.into())
}
fn hash(&self) -> String {
self.inner.hash().expect_throw("failed to get hash.")
}
fn route<R>(&self) -> Option<R>
where
R: Routable,
{
R::recognize(&self.pathname())
}
}
impl BrowserLocation {
fn new(history: BrowserHistory) -> Self {
Self {
inner: window().location(),
_history: history,
}
}
/// Returns the `href` of current [`Location`].
pub fn href(&self) -> String {
self.inner.href().expect_throw("failed to get href.")
}
/// Returns the `origin` of current [`Location`].
pub fn origin(&self) -> String {
self.inner.origin().expect_throw("failed to get origin.")
}
/// Returns the `protocol` property of current [`Location`].
pub fn protocol(&self) -> String {
self.inner
.protocol()
.expect_throw("failed to get protocol.")
}
/// Returns the `host` of current [`Location`].
pub fn host(&self) -> String {
self.inner.host().expect_throw("failed to get host.")
}
/// Returns the `hostname` of current [`Location`].
pub fn hostname(&self) -> String {
self.inner
.hostname()
.expect_throw("failed to get hostname.")
}
}
/// A [`History`] that is always available under a [`Router`](crate::Router).
#[derive(Clone, PartialEq)]
pub enum AnyHistory {
Browser(BrowserHistory),
}
/// The [`Location`] for [`AnyHistory`]
#[derive(Clone, PartialEq)]
pub enum AnyLocation {
Browser(BrowserLocation),
}
impl History for AnyHistory {
type Location = AnyLocation;
fn len(&self) -> usize {
let Self::Browser(self_) = self;
self_.len()
}
fn go(&self, delta: isize) {
let Self::Browser(self_) = self;
self_.go(delta)
}
fn push(&self, route: impl Routable) {
let Self::Browser(self_) = self;
self_.push(route)
}
fn replace(&self, route: impl Routable) {
let Self::Browser(self_) = self;
self_.replace(route)
}
fn push_with_state<T>(&self, route: impl Routable, state: T) -> HistoryResult<()>
where
T: Serialize + 'static,
{
let Self::Browser(self_) = self;
self_.push_with_state(route, state)
}
fn replace_with_state<T>(&self, route: impl Routable, state: T) -> HistoryResult<()>
where
T: Serialize + 'static,
{
let Self::Browser(self_) = self;
self_.replace_with_state(route, state)
}
fn push_with_query<Q>(&self, route: impl Routable, query: Q) -> HistoryResult<()>
where
Q: Serialize,
{
let Self::Browser(self_) = self;
self_.push_with_query(route, query)
}
fn replace_with_query<Q>(&self, route: impl Routable, query: Q) -> HistoryResult<()>
where
Q: Serialize,
{
let Self::Browser(self_) = self;
self_.replace_with_query(route, query)
}
fn push_with_query_and_state<Q, T>(
&self,
route: impl Routable,
query: Q,
state: T,
) -> HistoryResult<()>
where
Q: Serialize,
T: Serialize + 'static,
{
let Self::Browser(self_) = self;
self_.push_with_query_and_state(route, query, state)
}
fn replace_with_query_and_state<Q, T>(
&self,
route: impl Routable,
query: Q,
state: T,
) -> HistoryResult<()>
where
Q: Serialize,
T: Serialize + 'static,
{
let Self::Browser(self_) = self;
self_.replace_with_query_and_state(route, query, state)
}
fn listen<CB>(&self, callback: CB) -> HistoryListener
where
CB: Fn() + 'static,
{
let Self::Browser(self_) = self;
self_.listen(callback)
}
fn location(&self) -> Self::Location {
let Self::Browser(self_) = self;
AnyLocation::Browser(self_.location())
}
fn into_any_history(self) -> AnyHistory {
self
}
fn state<T>(&self) -> HistoryResult<T>
where
T: DeserializeOwned + 'static,
{
let Self::Browser(self_) = self;
self_.state()
}
}
impl Location for AnyLocation {
type History = AnyHistory;
fn pathname(&self) -> String {
let Self::Browser(self_) = self;
self_.pathname()
}
fn search(&self) -> String {
let Self::Browser(self_) = self;
self_.search()
}
fn query<T>(&self) -> HistoryResult<T>
where
T: DeserializeOwned,
{
let Self::Browser(self_) = self;
self_.query()
}
fn hash(&self) -> String {
let Self::Browser(self_) = self;
self_.hash()
}
fn route<R>(&self) -> Option<R>
where
R: Routable,
{
let Self::Browser(self_) = self;
self_.route()
}
}

View File

@ -0,0 +1,32 @@
//! Hooks to access router state and navigate between pages.
use crate::history::*;
use crate::routable::Routable;
use crate::router::RouterState;
use yew::prelude::*;
/// A hook to access the [`AnyHistory`] type.
pub fn use_history() -> Option<AnyHistory> {
let history_state = use_context::<RouterState>()?;
Some(history_state.history())
}
/// A hook to access the [`AnyLocation`] type.
pub fn use_location() -> Option<AnyLocation> {
Some(use_history()?.location())
}
/// A hook to access the current route.
///
/// This hook will return [`None`] if there's no available location or none of the routes match.
///
/// If your `Routable` has a `#[not_found]` route, you can use `.unwrap_or_default()` instead of
/// `.unwrap()` to unwrap.
pub fn use_route<R>() -> Option<R>
where
R: Routable + 'static,
{
use_location()?.route::<R>()
}

View File

@ -4,9 +4,9 @@
//! # Usage
//!
//! ```rust
//! # use yew::prelude::*;
//! # use yew::functional::*;
//! # use yew_router::prelude::*;
//! use yew::prelude::*;
//! use yew::functional::*;
//! use yew_router::prelude::*;
//!
//! #[derive(Debug, Clone, Copy, PartialEq, Routable)]
//! enum Route {
@ -19,22 +19,33 @@
//! NotFound,
//! }
//!
//! # #[function_component(Main)]
//! # fn app() -> Html {
//! html! {
//! <Router<Route> render={Router::render(switch)} />
//! #[function_component(Secure)]
//! fn secure() -> Html {
//! let history = use_history().unwrap();
//!
//! let onclick_callback = Callback::from(move |_| history.push(Route::Home));
//! html! {
//! <div>
//! <h1>{ "Secure" }</h1>
//! <button onclick={onclick_callback}>{ "Go Home" }</button>
//! </div>
//! }
//! }
//!
//! #[function_component(Main)]
//! fn app() -> Html {
//! html! {
//! <BrowserRouter>
//! <Switch<Route> render={Switch::render(switch)} />
//! </BrowserRouter>
//! }
//! }
//! # }
//!
//! fn switch(routes: &Route) -> Html {
//! let onclick_callback = Callback::from(|_| yew_router::push_route(Route::Home));
//! match routes {
//! Route::Home => html! { <h1>{ "Home" }</h1> },
//! Route::Secure => html! {
//! <div>
//! <h1>{ "Secure" }</h1>
//! <button onclick={onclick_callback}>{ "Go Home" }</button>
//! </div>
//! <Secure />
//! },
//! Route::NotFound => html! { <h1>{ "404" }</h1> },
//! }
@ -43,15 +54,13 @@
//!
//! # Internals
//!
//! The router keeps its own internal state which is used to store the current route and its associated data.
//! This allows the [Router] to be operated using the [service] with an API that
//! isn't cumbersome to use.
//! The router registers itself as a context provider and makes session history and location information
//! available via [`hooks`] or [`RouterScopeExt`](scope_ext::RouterScopeExt).
//!
//! # State
//!
//! The browser history API allows users to state associated with the route. This crate does
//! not expose or make use of it. It is instead recommended that a state management library like
//! [yewdux](https://github.com/intendednull/yewdux) be used.
//! The [`history`] API has a way access / store state associated with session history. Please
//! consule [`history.state()`](history::History::state) for detailed usage.
extern crate self as yew_router;
@ -59,23 +68,30 @@ extern crate self as yew_router;
#[path = "macro_helpers.rs"]
pub mod __macro;
pub mod components;
pub mod history;
pub mod hooks;
mod routable;
pub mod router;
mod service;
pub(crate) mod utils;
pub mod scope_ext;
pub mod switch;
pub mod utils;
pub use service::*;
pub use routable::Routable;
pub use router::{RenderFn, Router};
pub use routable::{AnyRoute, Routable};
pub use router::{BrowserRouter, Router};
pub use switch::{RenderFn, Switch};
pub mod prelude {
//! Prelude module to be imported when working with `yew-router`.
//!
//! This module re-exports the frequently used types from the crate.
pub use crate::components::Link;
pub use crate::components::{Link, Redirect};
pub use crate::history::*;
pub use crate::hooks::*;
pub use crate::scope_ext::RouterScopeExt;
#[doc(no_inline)]
pub use crate::Routable;
pub use crate::Router;
pub use crate::{BrowserRouter, Router};
pub use crate::Switch;
}

View File

@ -13,7 +13,7 @@ pub use yew_router_macro::Routable;
///
/// The functions exposed by this trait are **not** supposed to be consumed directly. Instead use
/// the functions exposed at the [crate's root][crate] to perform operations with the router.
pub trait Routable: Sized + Clone {
pub trait Routable: Clone + PartialEq {
/// Converts path to an instance of the routes enum.
fn from_path(path: &str, params: &HashMap<&str, &str>) -> Option<Self>;
@ -26,14 +26,56 @@ pub trait Routable: Sized + Clone {
/// The route to redirect to on 404
fn not_found_route() -> Option<Self>;
/// The current route
///
/// This is the cached result of [`recognize`]
fn current_route() -> Option<Self>;
/// Match a route based on the path
fn recognize(pathname: &str) -> Option<Self>;
/// Called when [`Router`](crate::Router) is destroyed.
fn cleanup() {}
}
/// A special route that accepts any route.
///
/// This can be used with [`History`](crate::History) and [`Location`](crate::Location)
/// when the type of [`Routable`] is unknown.
#[derive(Debug, Clone, PartialEq)]
pub struct AnyRoute {
path: String,
}
impl Routable for AnyRoute {
fn from_path(path: &str, params: &HashMap<&str, &str>) -> Option<Self> {
// No params allowed.
if params.is_empty() {
Some(Self {
path: path.to_string(),
})
} else {
None
}
}
fn to_path(&self) -> String {
self.path.to_string()
}
fn routes() -> Vec<&'static str> {
vec!["/*path"]
}
fn not_found_route() -> Option<Self> {
Some(Self {
path: "/404".to_string(),
})
}
fn recognize(pathname: &str) -> Option<Self> {
Some(Self {
path: pathname.to_string(),
})
}
}
impl AnyRoute {
pub fn new<S: Into<String>>(pathname: S) -> Self {
Self {
path: pathname.into(),
}
}
}

View File

@ -1,55 +1,32 @@
//! Router Component.
use crate::Routable;
use gloo::{console, events::EventListener};
use std::marker::PhantomData;
use std::rc::Rc;
use crate::prelude::*;
use yew::prelude::*;
/// Wraps `Rc` around `Fn` so it can be passed as a prop.
pub struct RenderFn<R>(Rc<dyn Fn(&R) -> Html>);
/// Props for [`Router`].
#[derive(Properties, PartialEq, Clone)]
pub struct RouterProps {
pub children: Children,
pub history: AnyHistory,
}
impl<R> RenderFn<R> {
/// Creates a new [`RenderFn`]
///
/// It is recommended that you use [`Router::render`] instead
pub fn new(value: impl Fn(&R) -> Html + 'static) -> Self {
Self(Rc::new(value))
/// A context for [`Router`]
#[derive(Clone)]
pub(crate) struct RouterState {
history: AnyHistory,
// Counter to force update.
ctr: u32,
}
impl RouterState {
pub fn history(&self) -> AnyHistory {
self.history.clone()
}
}
impl<T> Clone for RenderFn<T> {
fn clone(&self) -> Self {
Self(Rc::clone(&self.0))
}
}
impl<T> PartialEq for RenderFn<T> {
fn eq(&self, other: &Self) -> bool {
// https://github.com/rust-lang/rust-clippy/issues/6524
#[allow(clippy::vtable_address_comparisons)]
Rc::ptr_eq(&self.0, &other.0)
}
}
/// Props for [`Router`]
#[derive(Properties)]
pub struct RouterProps<R> {
/// Callback which returns [`Html`] to be rendered for the current route.
pub render: RenderFn<R>,
}
impl<R> Clone for RouterProps<R> {
fn clone(&self) -> Self {
Self {
render: self.render.clone(),
}
}
}
impl<R> PartialEq for RouterProps<R> {
fn eq(&self, other: &Self) -> bool {
self.render.eq(&other.render)
impl PartialEq for RouterState {
fn eq(&self, rhs: &Self) -> bool {
self.ctr == rhs.ctr
}
}
@ -58,70 +35,93 @@ pub enum Msg {
ReRender,
}
/// The router component.
/// The Router component.
///
/// When a route can't be matched, 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.
/// See the [crate level document][crate] for more information.
pub struct Router<R: Routable + 'static> {
#[allow(dead_code)] // only exists to drop listener on component drop
route_listener: EventListener,
_data: PhantomData<R>,
/// This provides [`History`] context to its children and switches.
///
/// You only need one `<Router />` for each application.
pub struct Router {
_listener: HistoryListener,
history: AnyHistory,
ctr: u32,
}
impl<R> Component for Router<R>
where
R: Routable + 'static,
{
impl Component for Router {
type Message = Msg;
type Properties = RouterProps<R>;
type Properties = RouterProps;
fn create(ctx: &Context<Self>) -> Self {
let link = ctx.link().clone();
let route_listener = EventListener::new(&yew::utils::window(), "popstate", move |_| {
link.send_message(Msg::ReRender)
});
let listener = ctx
.props()
.history
.listen(move || link.send_message(Msg::ReRender));
Self {
route_listener,
_data: PhantomData,
_listener: listener,
history: ctx.props().history.clone(),
ctr: 0,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::ReRender => true,
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let pathname = yew::utils::window().location().pathname().unwrap();
let route = R::recognize(&pathname);
match route {
Some(route) => (ctx.props().render.0)(&route),
None => {
console::warn!("no route matched");
html! {}
Msg::ReRender => {
self.ctr += 1;
true
}
}
}
fn destroy(&mut self, _ctx: &Context<Self>) {
R::cleanup();
fn changed(&mut self, ctx: &Context<Self>) -> bool {
let link = ctx.link().clone();
if self.history != ctx.props().history {
self._listener = ctx
.props()
.history
.listen(move || link.send_message(Msg::ReRender));
self.history = ctx.props().history.clone();
true
} else {
false
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let state = RouterState {
history: self.history.clone().into_any_history(),
ctr: self.ctr,
};
html! {
<ContextProvider<RouterState> context={state}>
{ctx.props().children.clone()}
</ContextProvider<RouterState>>
}
}
}
impl<R> Router<R>
where
R: Routable + Clone + 'static,
{
pub fn render<F>(func: F) -> RenderFn<R>
where
F: Fn(&R) -> Html + 'static,
{
RenderFn::new(func)
#[derive(Properties, PartialEq, Clone)]
pub struct BrowserRouterProps {
pub children: Children,
}
/// A [`Router`] thats provides history via [`BrowserHistory`].
///
/// This Router uses browser's native history to manipulate session history
/// and uses regular URL as route.
#[function_component(BrowserRouter)]
pub fn browser_router(props: &BrowserRouterProps) -> Html {
let history = use_state(BrowserHistory::new);
let children = props.children.clone();
html! {
<Router history={(*history).clone().into_any_history()}>
{children}
</Router>
}
}

View File

@ -0,0 +1,110 @@
use crate::history::*;
use crate::routable::Routable;
use crate::router::RouterState;
use yew::context::ContextHandle;
use yew::prelude::*;
/// A [`ContextHandle`] for [`add_history_listener`](RouterScopeExt::add_history_listener).
pub struct HistoryHandle {
_inner: ContextHandle<RouterState>,
}
/// An extension to [`Scope`](yew::html::Scope) that provides session history information.
///
/// You can access on `ctx.link()`
///
/// # Example
///
/// Below is an example of the implementation of the [`Link`](crate::components::Link) component.
///
/// ```
/// # use std::marker::PhantomData;
/// # use wasm_bindgen::UnwrapThrowExt;
/// # use yew::prelude::*;
/// # use yew_router::prelude::*;
/// # use yew_router::components::{LinkProps, Msg};
/// #
/// # pub struct Link<R: Routable + 'static> {
/// # _data: PhantomData<R>,
/// # }
/// #
/// impl<R: Routable + 'static> Component for Link<R> {
/// type Message = Msg;
/// type Properties = LinkProps<R>;
///
/// fn create(_ctx: &Context<Self>) -> Self {
/// Self { _data: PhantomData }
/// }
///
/// fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
/// match msg {
/// Msg::OnClick => {
/// ctx.link()
/// .history()
/// .expect_throw("failed to read history")
/// .push(ctx.props().to.clone());
/// false
/// }
/// }
/// }
///
/// fn view(&self, ctx: &Context<Self>) -> Html {
/// html! {
/// <a class={ctx.props().classes.clone()}
/// href={ctx.props().to.to_path()}
/// onclick={ctx.link().callback(|e: MouseEvent| {
/// e.prevent_default();
/// Msg::OnClick
/// })}
/// >
/// { ctx.props().children.clone() }
/// </a>
/// }
/// }
/// }
/// ```
pub trait RouterScopeExt {
/// Returns current [`History`].
fn history(&self) -> Option<AnyHistory>;
/// Returns current [`Location`].
fn location(&self) -> Option<AnyLocation>;
/// Returns current route.
fn route<R>(&self) -> Option<R>
where
R: Routable + 'static;
/// Adds a listener that gets notified when history changes.
///
/// # Note
///
/// [`HistoryHandle`] works like a normal [`ContextHandle`] and it unregisters the callback
/// when the handle is dropped. You need to keep the handle for as long as you need the
/// callback.
fn add_history_listener(&self, cb: Callback<AnyHistory>) -> Option<HistoryHandle>;
}
impl<COMP: Component> RouterScopeExt for yew::html::Scope<COMP> {
fn history(&self) -> Option<AnyHistory> {
self.context::<RouterState>(Callback::from(|_| {}))
.map(|(m, _)| m.history())
}
fn location(&self) -> Option<AnyLocation> {
self.history().map(|m| m.location())
}
fn route<R>(&self) -> Option<R>
where
R: Routable + 'static,
{
self.location()?.route()
}
fn add_history_listener(&self, cb: Callback<AnyHistory>) -> Option<HistoryHandle> {
self.context::<RouterState>(Callback::from(move |m: RouterState| cb.emit(m.history())))
.map(|(_, m)| HistoryHandle { _inner: m })
}
}

View File

@ -1,127 +0,0 @@
use crate::utils::base_url;
use crate::Routable;
use gloo::events::EventListener;
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsValue;
use web_sys::Event;
use yew::Callback;
/// Navigate to a specific route, pushing the new route onto the
/// user's history stack.
pub fn push_route(route: impl Routable) {
update_route_impl(route.to_path(), true)
}
/// Navigate to a specific route, replacing the current route on the
/// user's history stack.
pub fn replace_route(route: impl Routable) {
update_route_impl(route.to_path(), false)
}
/// Navigate to a specific route with query parameters, pushing the
/// new route onto the user's history stack.
///
/// This should be used in cases where [`Link`](crate::prelude::Link) is insufficient.
pub fn push_route_with_query<S>(
route: impl Routable,
query: S,
) -> Result<(), serde_urlencoded::ser::Error>
where
S: Serialize,
{
update_route_with_query_impl(route, query, true)
}
/// Navigate to a specific route with query parameters, replacing the
/// current route on the user's history stack.
pub fn replace_route_with_query<S>(
route: impl Routable,
query: S,
) -> Result<(), serde_urlencoded::ser::Error>
where
S: Serialize,
{
update_route_with_query_impl(route, query, false)
}
fn update_route_with_query_impl<S>(
route: impl Routable,
query: S,
push: bool,
) -> Result<(), serde_urlencoded::ser::Error>
where
S: Serialize,
{
let mut url = route.to_path();
let query = serde_urlencoded::to_string(query)?;
if !query.is_empty() {
url.push_str(&format!("?{}", query));
}
update_route_impl(url, push);
Ok(())
}
fn update_route_impl(url: String, push: bool) {
let history = yew::utils::window().history().expect("no history");
let base = base_url();
let path = match base {
Some(base) => {
let path = format!("{}{}", base, url);
if path.is_empty() {
"/".to_string()
} else {
path
}
}
None => url,
};
if push {
history
.push_state_with_url(&JsValue::NULL, "", Some(&path))
.expect("push history");
} else {
history
.replace_state_with_url(&JsValue::NULL, "", Some(&path))
.expect("replace history");
}
let event = Event::new("popstate").unwrap();
yew::utils::window()
.dispatch_event(&event)
.expect("dispatch");
}
pub fn parse_query<T>() -> Result<T, serde_urlencoded::de::Error>
where
T: for<'de> Deserialize<'de>,
{
let query = yew::utils::document().location().unwrap().search().unwrap();
serde_urlencoded::from_str(query.strip_prefix('?').unwrap_or(""))
}
pub fn current_route<R: Routable>() -> Option<R> {
R::current_route()
}
/// Handle for the router's path event listener
pub struct RouteListener {
// this exists so listener is dropped when handle is dropped
#[allow(dead_code)]
listener: EventListener,
}
/// Adds a listener which is called when the current route is changed.
///
/// The callback receives `Option<R>` so it can handle the error case itself.
pub fn attach_route_listener<R>(callback: Callback<Option<R>>) -> RouteListener
where
R: Routable + 'static,
{
let listener = EventListener::new(&yew::utils::window(), "popstate", move |_| {
callback.emit(current_route())
});
RouteListener { listener }
}

View File

@ -0,0 +1,120 @@
//! The [`Switch`] Component.
use std::marker::PhantomData;
use std::rc::Rc;
use gloo::console;
use wasm_bindgen::UnwrapThrowExt;
use yew::prelude::*;
use crate::prelude::*;
use crate::scope_ext::HistoryHandle;
/// Wraps `Rc` around `Fn` so it can be passed as a prop.
pub struct RenderFn<R>(Rc<dyn Fn(&R) -> Html>);
impl<R> RenderFn<R> {
/// Creates a new [`RenderFn`]
///
/// It is recommended that you use [`Switch::render`] instead
pub fn new(value: impl Fn(&R) -> Html + 'static) -> Self {
Self(Rc::new(value))
}
pub fn render(&self, route: &R) -> Html {
(self.0)(route)
}
}
impl<T> Clone for RenderFn<T> {
fn clone(&self) -> Self {
Self(Rc::clone(&self.0))
}
}
impl<T> PartialEq for RenderFn<T> {
fn eq(&self, other: &Self) -> bool {
// https://github.com/rust-lang/rust-clippy/issues/6524
#[allow(clippy::vtable_address_comparisons)]
Rc::ptr_eq(&self.0, &other.0)
}
}
/// Props for [`Switch`]
#[derive(Properties, PartialEq, Clone)]
pub struct SwitchProps<R>
where
R: Routable,
{
/// Callback which returns [`Html`] to be rendered for the current route.
pub render: RenderFn<R>,
}
#[doc(hidden)]
pub enum Msg {
ReRender,
}
/// 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.
/// 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.
/// See the [crate level document][crate] for more information.
pub struct Switch<R: Routable + 'static> {
_listener: HistoryHandle,
_phantom: PhantomData<R>,
}
impl<R> Component for Switch<R>
where
R: Routable + 'static,
{
type Message = Msg;
type Properties = SwitchProps<R>;
fn create(ctx: &Context<Self>) -> Self {
let link = ctx.link();
let listener = link
.add_history_listener(link.callback(move |_| Msg::ReRender))
.expect_throw("failed to create history handle. Do you have a router registered?");
Self {
_listener: listener,
_phantom: PhantomData,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::ReRender => true,
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let route = ctx.link().location().and_then(|m| m.route::<R>());
let children = match &route {
Some(ref route) => (ctx.props().render.0)(route),
None => {
console::warn!("no route matched");
Html::default()
}
};
html! {<>{children}</>}
}
}
impl<R> Switch<R>
where
R: Routable + Clone + 'static,
{
/// Creates a [`RenderFn`].
pub fn render<F>(func: F) -> RenderFn<R>
where
F: Fn(&R) -> Html + 'static,
{
RenderFn::new(func)
}
}

View File

@ -43,11 +43,8 @@ pub fn fetch_base_url() -> Option<String> {
#[cfg(test)]
mod tests {
use serde::Serialize;
use std::collections::HashMap;
use wasm_bindgen_test::wasm_bindgen_test as test;
use yew::utils::*;
use yew_router::parse_query;
use yew_router::prelude::*;
use yew_router::utils::*;
@ -81,34 +78,4 @@ mod tests {
.set_inner_html(r#"<base href="/base">"#);
assert_eq!(fetch_base_url(), Some("/base".to_string()));
}
#[derive(Serialize, Clone)]
struct QueryParams {
foo: String,
bar: u32,
}
#[test]
fn test_get_query_params() {
assert_eq!(
parse_query::<HashMap<String, String>>().unwrap(),
HashMap::new()
);
let query = QueryParams {
foo: "test".to_string(),
bar: 69,
};
yew_router::push_route_with_query(Routes::Home, query).unwrap();
let params: HashMap<String, String> = parse_query().unwrap();
assert_eq!(params, {
let mut map = HashMap::new();
map.insert("foo".to_string(), "test".to_string());
map.insert("bar".to_string(), "69".to_string());
map
});
}
}

View File

@ -0,0 +1,79 @@
use gloo::timers::future::TimeoutFuture;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
use yew_router::prelude::*;
use yew_router::AnyRoute;
use serde::{Deserialize, Serialize};
wasm_bindgen_test_configure!(run_in_browser);
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Query {
a: String,
b: u64,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct State {
i: String,
ii: u64,
}
#[test]
async fn history_works() {
let history = BrowserHistory::new();
assert_eq!(history.location().pathname(), "/");
history.push(AnyRoute::new("/path-a"));
assert_eq!(history.location().pathname(), "/path-a");
history.replace(AnyRoute::new("/path-b"));
assert_eq!(history.location().pathname(), "/path-b");
history.back();
TimeoutFuture::new(100).await;
assert_eq!(history.location().pathname(), "/");
history.forward();
TimeoutFuture::new(100).await;
assert_eq!(history.location().pathname(), "/path-b");
history
.push_with_query(
AnyRoute::new("/path"),
Query {
a: "something".to_string(),
b: 123,
},
)
.unwrap();
assert_eq!(history.location().pathname(), "/path");
assert_eq!(history.location().search(), "?a=something&b=123");
assert_eq!(
history.location().query::<Query>().unwrap(),
Query {
a: "something".to_string(),
b: 123,
}
);
history
.push_with_state(
AnyRoute::new("/path-c"),
State {
i: "something".to_string(),
ii: 123,
},
)
.unwrap();
assert_eq!(history.location().pathname(), "/path-c");
assert_eq!(
history.state::<State>().unwrap(),
State {
i: "something".to_string(),
ii: 123,
}
);
}

View File

@ -33,35 +33,43 @@ struct NoProps {
fn no(props: &NoProps) -> Html {
let route = props.id.to_string();
let location = use_location().unwrap();
html! {
<>
<div id="result-params">{ route }</div>
<div id="result-query">{ yew_router::parse_query::<Query>().unwrap().foo }</div>
<div id="result-query">{ location.query::<Query>().unwrap().foo }</div>
</>
}
}
#[function_component(Comp)]
fn component() -> Html {
let switch = Router::render(|routes| {
let replace_route = Callback::from(|_| {
yew_router::replace_route_with_query(
Routes::No { id: 2 },
Query {
foo: "bar".to_string(),
},
)
.unwrap();
let history = use_history().unwrap();
let switch = Switch::render(move |routes| {
let history_clone = history.clone();
let replace_route = Callback::from(move |_| {
history_clone
.replace_with_query(
Routes::No { id: 2 },
Query {
foo: "bar".to_string(),
},
)
.unwrap();
});
let push_route = Callback::from(|_| {
yew_router::push_route_with_query(
Routes::No { id: 3 },
Query {
foo: "baz".to_string(),
},
)
.unwrap();
let history_clone = history.clone();
let push_route = Callback::from(move |_| {
history_clone
.push_with_query(
Routes::No { id: 3 },
Query {
foo: "baz".to_string(),
},
)
.unwrap();
});
match routes {
@ -82,8 +90,16 @@ fn component() -> Html {
});
html! {
<Router<Routes> render={switch}>
</Router<Routes>>
<Switch<Routes> render={switch} />
}
}
#[function_component(Root)]
fn root() -> Html {
html! {
<BrowserRouter>
<Comp />
</BrowserRouter>
}
}
@ -97,7 +113,7 @@ fn component() -> Html {
// - 404 redirects
#[test]
fn router_works() {
yew::start_app_in_element::<Comp>(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<Root>(yew::utils::document().get_element_by_id("output").unwrap());
assert_eq!("Home", obtain_result_by_id("result"));

View File

@ -5,21 +5,20 @@ 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.
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.
## Usage
The Router component. It takes in a callback and renders the HTML based on the returned value of the callback. It is usually placed
at the top of the application.
You start by defining a `Route`.
Routes are defined by an `enum` which derives `Routable`. This enum must be `Clone + Sized.
Routes are defined as an `enum` which derives `Routable`. This enum must be `Clone + PartialEq`.
```rust
use yew_router::Routable;
use yew_router::prelude::*;
#[derive(Clone, Routable)]
#[derive(Clone, Routable, PartialEq)]
enum Route {
#[at("/")]
Home,
@ -31,19 +30,16 @@ enum Route {
}
```
The `Router` component takes the `Routable` enum as its type parameter, 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,
A `Route` is paired with a `<Switch />` 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.
`yew_router::current_route` is used to programmatically obtain the current route.
`yew_router::attach_route_listener` is used to attach a listener which is called every time route is changed.
```rust
use yew_router::{Router, Routable};
use yew::{Callback, function_component, html, Html};
use yew_router::prelude::*;;
use yew::prelude::*;
#[derive(Clone, Routable)]
#[derive(Clone, Routable, PartialEq)]
enum Route {
#[at("/")]
Home,
@ -54,45 +50,130 @@ enum Route {
NotFound,
}
#[function_component(Secure)]
fn secure() -> Html {
let history = use_history().unwrap();
let onclick_callback = Callback::from(move |_| history.push(Route::Home));
html! {
<div>
<h1>{ "Secure" }</h1>
<button onclick={onclick_callback}>{ "Go Home" }</button>
</div>
}
}
fn switch(routes: &Route) -> Html {
match routes {
Route::Home => html! { <h1>{ "Home" }</h1> },
Route::Secure => html! {
<Secure />
},
Route::NotFound => html! { <h1>{ "404" }</h1> },
}
}
#[function_component(Main)]
fn app() -> Html {
html! {
<Router<Route> render={Router::render(switch)} />
}
}
fn switch(route: &Route) -> Html {
match route {
Route::Home => html! { <h1>{ "Home" }</h1> },
Route::Secure => {
let callback = Callback::from(|_| yew_router::push_route(Route::Home));
html! {
<div>
<h1>{ "Secure" }</h1>
<button onclick={callback}>{ "Go Home" }</button>
</div>
}
},
Route::NotFound => html! { <h1>{ "404" }</h1> },
<Switch<Route> render={Switch::render(switch)} />
}
}
```
Finally, you need to register the `<Router />` component as a context.
`<Router />` provides session history information to its children.
When using `yew-router` in browser environment, `<BrowserRouter />` is
recommended.
```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! {
<div>
<h1>{ "Secure" }</h1>
<button onclick={onclick_callback}>{ "Go Home" }</button>
</div>
}
}
fn switch(routes: &Route) -> Html {
match routes {
Route::Home => html! { <h1>{ "Home" }</h1> },
Route::Secure => html! {
<Secure />
},
Route::NotFound => html! { <h1>{ "404" }</h1> },
}
}
#[function_component(Main)]
fn app() -> Html {
html! {
<BrowserRouter>
<Switch<Route> render={Switch::render(switch)} />
</BrowserRouter>
}
}
```
### 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()`.
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`.
You can access them using the following hooks:
- `use_history`
- `use_location`
#### `BrowserHistory` and `BrowserLocation`
These are only available when `<BrowserRouter />` 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 `<a>` element), the `yew_router::push_route` function, or the `yew_router::replace_route` function, which replaces the current page in the user's browser history instead of pushing a new one onto the stack.
To navigate between pages, use either a `Link` component (which renders a `<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.
### Query Parameters
#### Specifying query parameters when navigating
In order to specify query parameters when navigating to a new route, use either `yew_router::push_route_with_query` or the `yew_router::replace_route_with_query` functions.
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
`yew_router::parse_query` is used to obtain the query parameters.
`location.query` is used to obtain the query parameters.
It uses `serde` to deserialize the parameters from query string in the URL.
## Relevant examples