mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
* 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:
parent
6edb135a4d
commit
f43760e3ca
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>{ "e.author.name }</strong>
|
||||
</Link<Route>>
|
||||
<p class="is-family-secondary">
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
//! Components to interface with [Router][crate::Router].
|
||||
|
||||
mod link;
|
||||
mod redirect;
|
||||
pub use link::*;
|
||||
pub use redirect::*;
|
||||
|
||||
31
packages/yew-router/src/components/redirect.rs
Normal file
31
packages/yew-router/src/components/redirect.rs
Normal 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()
|
||||
}
|
||||
684
packages/yew-router/src/history.rs
Normal file
684
packages/yew-router/src/history.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
32
packages/yew-router/src/hooks.rs
Normal file
32
packages/yew-router/src/hooks.rs
Normal 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>()
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
110
packages/yew-router/src/scope_ext.rs
Normal file
110
packages/yew-router/src/scope_ext.rs
Normal 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 })
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
120
packages/yew-router/src/switch.rs
Normal file
120
packages/yew-router/src/switch.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
79
packages/yew-router/tests/history.rs
Normal file
79
packages/yew-router/tests/history.rs
Normal 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,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -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"));
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user