mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Add HashRouter, basename and use gloo-history (#2239)
* Migrate to gloo-history. * Fix docs wording. * Add basename handling to navigator. * Fix basename handling. * Add lints and tests. * Fix wording. * Fix docs. * Fix pr-flow. * Fix some documentation. * Add Navigator Kind. * Remove history.rs * Add documentation for navigator & Concrete Router Props. * Update website/docs/concepts/router.md Co-authored-by: Julius Lungys <32368314+voidpumpkin@users.noreply.github.com> * Add docs about basename. * Update documentation. * Fix docs. Co-authored-by: Julius Lungys <32368314+voidpumpkin@users.noreply.github.com>
This commit is contained in:
parent
5b0bbd55c9
commit
d8ec50150e
@ -13,7 +13,7 @@ pub enum Msg {
|
||||
|
||||
pub struct PostList {
|
||||
page: u64,
|
||||
_listener: HistoryListener,
|
||||
_listener: LocationHandle,
|
||||
}
|
||||
|
||||
fn current_page(ctx: &Context<PostList>) -> u64 {
|
||||
@ -28,9 +28,10 @@ impl Component for PostList {
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let link = ctx.link().clone();
|
||||
let listener = ctx.link().history().unwrap().listen(move || {
|
||||
link.send_message(Msg::PageUpdated);
|
||||
});
|
||||
let listener = ctx
|
||||
.link()
|
||||
.add_location_listener(link.callback(move |_| Msg::PageUpdated))
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
page: current_page(ctx),
|
||||
|
||||
@ -88,6 +88,22 @@ fn parse_variants_attributes(
|
||||
};
|
||||
|
||||
let lit = attr.parse_args::<LitStr>()?;
|
||||
let val = lit.value();
|
||||
|
||||
if val.find('#').is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
lit,
|
||||
"You cannot use `#` in your routes. Please consider `HashRouter` instead.",
|
||||
));
|
||||
}
|
||||
|
||||
if !val.starts_with('/') {
|
||||
return Err(syn::Error::new_spanned(
|
||||
lit,
|
||||
"relative paths are not supported at this moment.",
|
||||
));
|
||||
}
|
||||
|
||||
ats.push(lit);
|
||||
|
||||
for attr in attrs.iter() {
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
#[derive(yew_router::Routable)]
|
||||
enum Routes {
|
||||
#[at("one")]
|
||||
One,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@ -0,0 +1,5 @@
|
||||
error: relative paths are not supported at this moment.
|
||||
--> tests/routable_derive/relative-path-fail.rs:3:10
|
||||
|
|
||||
3 | #[at("one")]
|
||||
| ^^^^^
|
||||
@ -0,0 +1,7 @@
|
||||
#[derive(yew_router::Routable)]
|
||||
enum Routes {
|
||||
#[at("/#/one")]
|
||||
One,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@ -0,0 +1,5 @@
|
||||
error: You cannot use `#` in your routes. Please consider `HashRouter` instead.
|
||||
--> tests/routable_derive/route-with-hash-fail.rs:3:10
|
||||
|
|
||||
3 | #[at("/#/one")]
|
||||
| ^^^^^^^^
|
||||
@ -20,7 +20,7 @@ gloo-utils = "0.1"
|
||||
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
gloo = { version = "0.4", features = ["futures"] }
|
||||
gloo = { version = "0.5", features = ["futures"] }
|
||||
route-recognizer = "0.3"
|
||||
serde = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
|
||||
@ -5,7 +5,7 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
use yew::prelude::*;
|
||||
use yew::virtual_dom::AttrValue;
|
||||
|
||||
use crate::history::{BrowserHistory, History};
|
||||
use crate::navigator::NavigatorKind;
|
||||
use crate::scope_ext::RouterScopeExt;
|
||||
use crate::Routable;
|
||||
|
||||
@ -63,13 +63,16 @@ where
|
||||
match msg {
|
||||
Msg::OnClick => {
|
||||
let LinkProps { to, query, .. } = ctx.props();
|
||||
let history = ctx.link().history().expect_throw("failed to read history");
|
||||
let navigator = ctx
|
||||
.link()
|
||||
.navigator()
|
||||
.expect_throw("failed to get navigator");
|
||||
match query {
|
||||
None => {
|
||||
history.push(to.clone());
|
||||
navigator.push(to.clone());
|
||||
}
|
||||
Some(data) => {
|
||||
history
|
||||
navigator
|
||||
.push_with_query(to.clone(), data.clone())
|
||||
.expect_throw("failed push history with query");
|
||||
}
|
||||
@ -91,7 +94,20 @@ where
|
||||
e.prevent_default();
|
||||
Msg::OnClick
|
||||
});
|
||||
let href: AttrValue = BrowserHistory::route_to_url(to).into();
|
||||
|
||||
let navigator = ctx
|
||||
.link()
|
||||
.navigator()
|
||||
.expect_throw("failed to get navigator");
|
||||
let href: AttrValue = {
|
||||
let href = navigator.route_to_url(to);
|
||||
|
||||
match navigator.kind() {
|
||||
NavigatorKind::Hash => format!("#{}", href).into(),
|
||||
_ => href,
|
||||
}
|
||||
.into()
|
||||
};
|
||||
html! {
|
||||
<a class={classes}
|
||||
{href}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::history::History;
|
||||
use crate::hooks::use_history;
|
||||
use crate::hooks::use_navigator;
|
||||
use crate::Routable;
|
||||
|
||||
/// Props for [`Redirect`]
|
||||
@ -18,7 +17,7 @@ pub fn redirect<R>(props: &RedirectProps<R>) -> Html
|
||||
where
|
||||
R: Routable + 'static,
|
||||
{
|
||||
let history = use_history().expect_throw("failed to read history.");
|
||||
let history = use_navigator().expect_throw("failed to read history.");
|
||||
|
||||
let target_route = props.to.clone();
|
||||
use_effect(move || {
|
||||
|
||||
@ -1,684 +0,0 @@
|
||||
//! 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 gloo_utils::window;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::{JsValue, UnwrapThrowExt};
|
||||
use yew::callback::Callback;
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub(crate) 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()
|
||||
}
|
||||
}
|
||||
@ -1,32 +1,37 @@
|
||||
//! Hooks to access router state and navigate between pages.
|
||||
|
||||
use crate::history::*;
|
||||
use crate::navigator::Navigator;
|
||||
use crate::routable::Routable;
|
||||
use crate::router::RouterState;
|
||||
use crate::router::{LocationContext, NavigatorContext};
|
||||
|
||||
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 [`Navigator`].
|
||||
pub fn use_navigator() -> Option<Navigator> {
|
||||
use_context::<NavigatorContext>().map(|m| m.navigator())
|
||||
}
|
||||
|
||||
/// A hook to access the [`AnyLocation`] type.
|
||||
pub fn use_location() -> Option<AnyLocation> {
|
||||
Some(use_history()?.location())
|
||||
/// A hook to access the current [`Location`].
|
||||
pub fn use_location() -> Option<Location> {
|
||||
Some(use_context::<LocationContext>()?.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.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// 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>()
|
||||
let navigator = use_navigator()?;
|
||||
let location = use_location()?;
|
||||
let path = navigator.strip_basename(location.path().into());
|
||||
|
||||
R::recognize(&path)
|
||||
}
|
||||
|
||||
@ -21,9 +21,9 @@
|
||||
//!
|
||||
//! #[function_component(Secure)]
|
||||
//! fn secure() -> Html {
|
||||
//! let history = use_history().unwrap();
|
||||
//! let navigator = use_navigator().unwrap();
|
||||
//!
|
||||
//! let onclick_callback = Callback::from(move |_| history.push(Route::Home));
|
||||
//! let onclick_callback = Callback::from(move |_| navigator.push(Route::Home));
|
||||
//! html! {
|
||||
//! <div>
|
||||
//! <h1>{ "Secure" }</h1>
|
||||
@ -54,13 +54,13 @@
|
||||
//!
|
||||
//! # Internals
|
||||
//!
|
||||
//! The router registers itself as a context provider and makes session history and location information
|
||||
//! The router registers itself as a context provider and makes location information and navigator
|
||||
//! available via [`hooks`] or [`RouterScopeExt`](scope_ext::RouterScopeExt).
|
||||
//!
|
||||
//! # State
|
||||
//!
|
||||
//! The [`history`] API has a way access / store state associated with session history. Please
|
||||
//! consule [`history.state()`](history::History::state) for detailed usage.
|
||||
//! The [`Location`] API has a way access / store state associated with session history. Please
|
||||
//! consult [`location.state()`](crate::history::Lcation::state) for detailed usage.
|
||||
|
||||
extern crate self as yew_router;
|
||||
|
||||
@ -68,8 +68,8 @@ extern crate self as yew_router;
|
||||
#[path = "macro_helpers.rs"]
|
||||
pub mod __macro;
|
||||
pub mod components;
|
||||
pub mod history;
|
||||
pub mod hooks;
|
||||
pub mod navigator;
|
||||
mod routable;
|
||||
pub mod router;
|
||||
pub mod scope_ext;
|
||||
@ -77,21 +77,31 @@ pub mod switch;
|
||||
pub mod utils;
|
||||
|
||||
pub use routable::{AnyRoute, Routable};
|
||||
pub use router::{BrowserRouter, Router};
|
||||
pub use router::{BrowserRouter, HashRouter, Router};
|
||||
pub use switch::{RenderFn, Switch};
|
||||
|
||||
pub mod history {
|
||||
//! A module that provides universal session history and location information.
|
||||
|
||||
pub use gloo::history::{
|
||||
AnyHistory, BrowserHistory, HashHistory, History, HistoryError, HistoryResult, Location,
|
||||
MemoryHistory,
|
||||
};
|
||||
}
|
||||
|
||||
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, Redirect};
|
||||
pub use crate::history::*;
|
||||
pub use crate::history::Location;
|
||||
pub use crate::hooks::*;
|
||||
pub use crate::scope_ext::RouterScopeExt;
|
||||
pub use crate::navigator::{NavigationError, NavigationResult, Navigator};
|
||||
pub use crate::scope_ext::{LocationHandle, NavigatorHandle, RouterScopeExt};
|
||||
#[doc(no_inline)]
|
||||
pub use crate::Routable;
|
||||
pub use crate::{BrowserRouter, Router};
|
||||
pub use crate::{BrowserRouter, HashRouter, Router};
|
||||
|
||||
pub use crate::Switch;
|
||||
}
|
||||
|
||||
177
packages/yew-router/src/navigator.rs
Normal file
177
packages/yew-router/src/navigator.rs
Normal file
@ -0,0 +1,177 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::history::{AnyHistory, History, HistoryError, HistoryResult};
|
||||
use crate::routable::Routable;
|
||||
|
||||
pub type NavigationError = HistoryError;
|
||||
pub type NavigationResult<T> = HistoryResult<T>;
|
||||
|
||||
/// The kind of Navigator Provider.
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum NavigatorKind {
|
||||
/// Browser History.
|
||||
Browser,
|
||||
/// Hash History.
|
||||
Hash,
|
||||
/// Memory History.
|
||||
Memory,
|
||||
}
|
||||
|
||||
/// A struct to navigate between locations.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Navigator {
|
||||
inner: AnyHistory,
|
||||
basename: Option<String>,
|
||||
}
|
||||
|
||||
impl Navigator {
|
||||
pub(crate) fn new(history: AnyHistory, basename: Option<String>) -> Self {
|
||||
Self {
|
||||
inner: history,
|
||||
basename,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns basename of current navigator.
|
||||
pub fn basename(&self) -> Option<&str> {
|
||||
self.basename.as_deref()
|
||||
}
|
||||
|
||||
/// Navigate back 1 page.
|
||||
pub fn back(&self) {
|
||||
self.go(-1);
|
||||
}
|
||||
|
||||
/// Navigate forward 1 page.
|
||||
pub fn forward(&self) {
|
||||
self.go(1);
|
||||
}
|
||||
|
||||
/// Navigate to a specific page with a `delta` relative to current page.
|
||||
///
|
||||
/// See: <https://developer.mozilla.org/en-US/docs/Web/API/History/go>
|
||||
pub fn go(&self, delta: isize) {
|
||||
self.inner.go(delta);
|
||||
}
|
||||
|
||||
/// Pushes a [`Routable`] entry.
|
||||
pub fn push(&self, route: impl Routable) {
|
||||
self.inner.push(self.route_to_url(route));
|
||||
}
|
||||
|
||||
/// Replaces the current history entry with provided [`Routable`] and [`None`] state.
|
||||
pub fn replace(&self, route: impl Routable) {
|
||||
self.inner.replace(self.route_to_url(route));
|
||||
}
|
||||
|
||||
/// Pushes a [`Routable`] entry with state.
|
||||
pub fn push_with_state<T>(&self, route: impl Routable, state: T)
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.inner.push_with_state(self.route_to_url(route), state);
|
||||
}
|
||||
|
||||
/// Replaces the current history entry with provided [`Routable`] and state.
|
||||
pub fn replace_with_state<T>(&self, route: impl Routable, state: T)
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.inner
|
||||
.replace_with_state(self.route_to_url(route), state);
|
||||
}
|
||||
|
||||
/// Same as `.push()` but affix the queries to the end of the route.
|
||||
pub fn push_with_query<Q>(&self, route: impl Routable, query: Q) -> NavigationResult<()>
|
||||
where
|
||||
Q: Serialize,
|
||||
{
|
||||
self.inner.push_with_query(self.route_to_url(route), query)
|
||||
}
|
||||
|
||||
/// Same as `.replace()` but affix the queries to the end of the route.
|
||||
pub fn replace_with_query<Q>(&self, route: impl Routable, query: Q) -> NavigationResult<()>
|
||||
where
|
||||
Q: Serialize,
|
||||
{
|
||||
self.inner
|
||||
.replace_with_query(self.route_to_url(route), query)
|
||||
}
|
||||
|
||||
/// Same as `.push_with_state()` but affix the queries to the end of the route.
|
||||
pub fn push_with_query_and_state<Q, T>(
|
||||
&self,
|
||||
route: impl Routable,
|
||||
query: Q,
|
||||
state: T,
|
||||
) -> NavigationResult<()>
|
||||
where
|
||||
Q: Serialize,
|
||||
T: 'static,
|
||||
{
|
||||
self.inner
|
||||
.push_with_query_and_state(self.route_to_url(route), query, state)
|
||||
}
|
||||
|
||||
/// Same as `.replace_with_state()` but affix the queries to the end of the route.
|
||||
pub fn replace_with_query_and_state<Q, T>(
|
||||
&self,
|
||||
route: impl Routable,
|
||||
query: Q,
|
||||
state: T,
|
||||
) -> NavigationResult<()>
|
||||
where
|
||||
Q: Serialize,
|
||||
T: 'static,
|
||||
{
|
||||
self.inner
|
||||
.replace_with_query_and_state(self.route_to_url(route), query, state)
|
||||
}
|
||||
|
||||
/// Returns the Navigator kind.
|
||||
pub fn kind(&self) -> NavigatorKind {
|
||||
match &self.inner {
|
||||
AnyHistory::Browser(_) => NavigatorKind::Browser,
|
||||
AnyHistory::Hash(_) => NavigatorKind::Hash,
|
||||
AnyHistory::Memory(_) => NavigatorKind::Memory,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn route_to_url(&self, route: impl Routable) -> Cow<'static, str> {
|
||||
let url = route.to_path();
|
||||
|
||||
let path = match self.basename() {
|
||||
Some(base) => {
|
||||
let path = format!("{}{}", base, url);
|
||||
if path.is_empty() {
|
||||
Cow::from("/")
|
||||
} else {
|
||||
path.into()
|
||||
}
|
||||
}
|
||||
None => url.into(),
|
||||
};
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub(crate) fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> {
|
||||
match self.basename() {
|
||||
Some(m) => {
|
||||
let mut path = path
|
||||
.strip_prefix(m)
|
||||
.map(|m| Cow::from(m.to_owned()))
|
||||
.unwrap_or(path);
|
||||
|
||||
if !path.starts_with('/') {
|
||||
path = format!("/{}", m).into();
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
None => path,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,81 +1,103 @@
|
||||
//! Router Component.
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::history::{AnyHistory, BrowserHistory, HashHistory, History, Location};
|
||||
use crate::navigator::Navigator;
|
||||
use crate::utils::{base_url, strip_slash_suffix};
|
||||
use yew::prelude::*;
|
||||
use yew::virtual_dom::AttrValue;
|
||||
|
||||
/// Props for [`Router`].
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct RouterProps {
|
||||
pub children: Children,
|
||||
pub history: AnyHistory,
|
||||
#[prop_or_default]
|
||||
pub basename: Option<AttrValue>,
|
||||
}
|
||||
|
||||
/// A context for [`Router`]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RouterState {
|
||||
history: AnyHistory,
|
||||
pub(crate) struct LocationContext {
|
||||
location: Location,
|
||||
// Counter to force update.
|
||||
ctr: u32,
|
||||
}
|
||||
|
||||
impl RouterState {
|
||||
pub fn history(&self) -> AnyHistory {
|
||||
self.history.clone()
|
||||
impl LocationContext {
|
||||
pub fn location(&self) -> Location {
|
||||
self.location.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RouterState {
|
||||
impl PartialEq for LocationContext {
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
self.ctr == rhs.ctr
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum RouterStateAction {
|
||||
Navigate,
|
||||
ReplaceHistory(AnyHistory),
|
||||
}
|
||||
|
||||
impl Reducible for RouterState {
|
||||
type Action = RouterStateAction;
|
||||
impl Reducible for LocationContext {
|
||||
type Action = Location;
|
||||
|
||||
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
|
||||
let history = match action {
|
||||
RouterStateAction::Navigate => self.history(),
|
||||
RouterStateAction::ReplaceHistory(m) => m,
|
||||
};
|
||||
|
||||
Self {
|
||||
history,
|
||||
location: action,
|
||||
ctr: self.ctr + 1,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) struct NavigatorContext {
|
||||
navigator: Navigator,
|
||||
}
|
||||
|
||||
impl NavigatorContext {
|
||||
pub fn navigator(&self) -> Navigator {
|
||||
self.navigator.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// The Router component.
|
||||
///
|
||||
/// This provides [`History`] context to its children and switches.
|
||||
/// This provides location and navigator context to its children and switches.
|
||||
///
|
||||
/// If you are building a web application, you may want to consider using [`BrowserRouter`] instead.
|
||||
///
|
||||
/// You only need one `<Router />` for each application.
|
||||
#[function_component(Router)]
|
||||
pub fn router(props: &RouterProps) -> Html {
|
||||
let RouterProps { history, children } = props.clone();
|
||||
let RouterProps {
|
||||
history,
|
||||
children,
|
||||
basename,
|
||||
} = props.clone();
|
||||
|
||||
let state = use_reducer(|| RouterState {
|
||||
history: history.clone(),
|
||||
let loc_ctx = use_reducer(|| LocationContext {
|
||||
location: history.location(),
|
||||
ctr: 0,
|
||||
});
|
||||
|
||||
let basename = basename.map(|m| strip_slash_suffix(&m).to_string());
|
||||
let navi_ctx = NavigatorContext {
|
||||
navigator: Navigator::new(history.clone(), basename),
|
||||
};
|
||||
|
||||
{
|
||||
let state_dispatcher = state.dispatcher();
|
||||
let loc_ctx_dispatcher = loc_ctx.dispatcher();
|
||||
|
||||
use_effect_with_deps(
|
||||
move |history| {
|
||||
state_dispatcher.dispatch(RouterStateAction::ReplaceHistory(history.clone()));
|
||||
let history = history.clone();
|
||||
// Force location update when history changes.
|
||||
loc_ctx_dispatcher.dispatch(history.location());
|
||||
|
||||
let listener =
|
||||
history.listen(move || state_dispatcher.dispatch(RouterStateAction::Navigate));
|
||||
let history_cb = {
|
||||
let history = history.clone();
|
||||
move || loc_ctx_dispatcher.dispatch(history.location())
|
||||
};
|
||||
|
||||
let listener = history.listen(history_cb);
|
||||
|
||||
// We hold the listener in the destructor.
|
||||
move || {
|
||||
@ -87,28 +109,61 @@ pub fn router(props: &RouterProps) -> Html {
|
||||
}
|
||||
|
||||
html! {
|
||||
<ContextProvider<RouterState> context={(*state).clone()}>
|
||||
{children}
|
||||
</ContextProvider<RouterState>>
|
||||
<ContextProvider<NavigatorContext> context={navi_ctx}>
|
||||
<ContextProvider<LocationContext> context={(*loc_ctx).clone()}>
|
||||
{children}
|
||||
</ContextProvider<LocationContext>>
|
||||
</ContextProvider<NavigatorContext>>
|
||||
}
|
||||
}
|
||||
|
||||
/// Props for [`BrowserRouter`] and [`HashRouter`].
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct BrowserRouterProps {
|
||||
pub struct ConcreteRouterProps {
|
||||
pub children: Children,
|
||||
#[prop_or_default]
|
||||
pub basename: Option<AttrValue>,
|
||||
}
|
||||
|
||||
/// A [`Router`] thats provides history via [`BrowserHistory`].
|
||||
/// A [`Router`] that provides location information and navigator via [`BrowserHistory`].
|
||||
///
|
||||
/// This Router uses browser's native history to manipulate session history
|
||||
/// and uses regular URL as route.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// The router will by default use the value declared in `<base href="..." />` as its basename.
|
||||
/// You may also specify a different basename with props.
|
||||
#[function_component(BrowserRouter)]
|
||||
pub fn browser_router(props: &BrowserRouterProps) -> Html {
|
||||
let history = use_state(BrowserHistory::new);
|
||||
let children = props.children.clone();
|
||||
pub fn browser_router(props: &ConcreteRouterProps) -> Html {
|
||||
let ConcreteRouterProps { children, basename } = props.clone();
|
||||
let history = use_state(|| AnyHistory::from(BrowserHistory::new()));
|
||||
|
||||
// We acknowledge based in `<base href="..." />`
|
||||
let basename = basename.map(|m| m.to_string()).or_else(base_url);
|
||||
|
||||
html! {
|
||||
<Router history={(*history).clone().into_any_history()}>
|
||||
<Router history={(*history).clone()} {basename}>
|
||||
{children}
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Router`] that provides location information and navigator via [`HashHistory`].
|
||||
///
|
||||
/// This Router uses browser's native history to manipulate session history
|
||||
/// and stores route in hash fragment.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// Prefer [`BrowserRouter`] whenever possible and use this as a last resort.
|
||||
#[function_component(HashRouter)]
|
||||
pub fn hash_router(props: &ConcreteRouterProps) -> Html {
|
||||
let ConcreteRouterProps { children, basename } = props.clone();
|
||||
let history = use_state(|| AnyHistory::from(HashHistory::new()));
|
||||
|
||||
html! {
|
||||
<Router history={(*history).clone()} {basename}>
|
||||
{children}
|
||||
</Router>
|
||||
}
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
use crate::history::*;
|
||||
use crate::history::Location;
|
||||
use crate::navigator::Navigator;
|
||||
use crate::routable::Routable;
|
||||
use crate::router::RouterState;
|
||||
use crate::router::{LocationContext, NavigatorContext};
|
||||
|
||||
use yew::context::ContextHandle;
|
||||
use yew::prelude::*;
|
||||
|
||||
/// A [`ContextHandle`] for [`add_history_listener`](RouterScopeExt::add_history_listener).
|
||||
pub struct HistoryHandle {
|
||||
_inner: ContextHandle<RouterState>,
|
||||
/// A [`ContextHandle`] for [`add_location_listener`](RouterScopeExt::add_location_listener).
|
||||
pub struct LocationHandle {
|
||||
_inner: ContextHandle<LocationContext>,
|
||||
}
|
||||
|
||||
/// An extension to [`Scope`](yew::html::Scope) that provides session history information.
|
||||
/// A [`ContextHandle`] for [`add_navigator_listener`](RouterScopeExt::add_navigator_listener).
|
||||
pub struct NavigatorHandle {
|
||||
_inner: ContextHandle<NavigatorContext>,
|
||||
}
|
||||
|
||||
/// An extension to [`Scope`](yew::html::Scope) that provides location information and navigator
|
||||
/// access.
|
||||
///
|
||||
/// You can access on `ctx.link()`
|
||||
/// You can access them on `ctx.link()`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@ -41,8 +48,8 @@ pub struct HistoryHandle {
|
||||
/// match msg {
|
||||
/// Msg::OnClick => {
|
||||
/// ctx.link()
|
||||
/// .history()
|
||||
/// .expect_throw("failed to read history")
|
||||
/// .navigator()
|
||||
/// .expect_throw("failed to get navigator.")
|
||||
/// .push(ctx.props().to.clone());
|
||||
/// false
|
||||
/// }
|
||||
@ -65,46 +72,70 @@ pub struct HistoryHandle {
|
||||
/// }
|
||||
/// ```
|
||||
pub trait RouterScopeExt {
|
||||
/// Returns current [`History`].
|
||||
fn history(&self) -> Option<AnyHistory>;
|
||||
/// Returns current [`Navigator`].
|
||||
fn navigator(&self) -> Option<Navigator>;
|
||||
|
||||
/// Returns current [`Location`].
|
||||
fn location(&self) -> Option<AnyLocation>;
|
||||
fn location(&self) -> Option<Location>;
|
||||
|
||||
/// Returns current route.
|
||||
fn route<R>(&self) -> Option<R>
|
||||
where
|
||||
R: Routable + 'static;
|
||||
|
||||
/// Adds a listener that gets notified when history changes.
|
||||
/// Adds a listener that gets notified when location changes.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// [`HistoryHandle`] works like a normal [`ContextHandle`] and it unregisters the callback
|
||||
/// [`LocationHandle`] 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>;
|
||||
fn add_location_listener(&self, cb: Callback<Location>) -> Option<LocationHandle>;
|
||||
|
||||
/// Adds a listener that gets notified when navigator changes.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// [`NavigatorHandle`] 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_navigator_listener(&self, cb: Callback<Navigator>) -> Option<NavigatorHandle>;
|
||||
}
|
||||
|
||||
impl<COMP: Component> RouterScopeExt for yew::html::Scope<COMP> {
|
||||
fn history(&self) -> Option<AnyHistory> {
|
||||
self.context::<RouterState>(Callback::from(|_| {}))
|
||||
.map(|(m, _)| m.history())
|
||||
fn navigator(&self) -> Option<Navigator> {
|
||||
self.context::<NavigatorContext>(Callback::from(|_| {}))
|
||||
.map(|(m, _)| m.navigator())
|
||||
}
|
||||
|
||||
fn location(&self) -> Option<AnyLocation> {
|
||||
self.history().map(|m| m.location())
|
||||
fn location(&self) -> Option<Location> {
|
||||
self.context::<LocationContext>(Callback::from(|_| {}))
|
||||
.map(|(m, _)| m.location())
|
||||
}
|
||||
|
||||
fn route<R>(&self) -> Option<R>
|
||||
where
|
||||
R: Routable + 'static,
|
||||
{
|
||||
self.location()?.route()
|
||||
let navigator = self.navigator()?;
|
||||
let location = self.location()?;
|
||||
|
||||
let path = navigator.strip_basename(location.path().into());
|
||||
|
||||
R::recognize(&path)
|
||||
}
|
||||
|
||||
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 })
|
||||
fn add_location_listener(&self, cb: Callback<Location>) -> Option<LocationHandle> {
|
||||
self.context::<LocationContext>(Callback::from(move |m: LocationContext| {
|
||||
cb.emit(m.location())
|
||||
}))
|
||||
.map(|(_, m)| LocationHandle { _inner: m })
|
||||
}
|
||||
|
||||
fn add_navigator_listener(&self, cb: Callback<Navigator>) -> Option<NavigatorHandle> {
|
||||
self.context::<NavigatorContext>(Callback::from(move |m: NavigatorContext| {
|
||||
cb.emit(m.navigator())
|
||||
}))
|
||||
.map(|(_, m)| NavigatorHandle { _inner: m })
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::scope_ext::HistoryHandle;
|
||||
use crate::scope_ext::LocationHandle;
|
||||
|
||||
/// Wraps `Rc` around `Fn` so it can be passed as a prop.
|
||||
pub struct RenderFn<R>(Rc<dyn Fn(&R) -> Html>);
|
||||
@ -63,7 +63,7 @@ pub enum Msg {
|
||||
/// stating that no route can be matched.
|
||||
/// See the [crate level document][crate] for more information.
|
||||
pub struct Switch<R: Routable + 'static> {
|
||||
_listener: HistoryHandle,
|
||||
_listener: LocationHandle,
|
||||
_phantom: PhantomData<R>,
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ where
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let link = ctx.link();
|
||||
let listener = link
|
||||
.add_history_listener(link.callback(move |_| Msg::ReRender))
|
||||
.add_location_listener(link.callback(move |_| Msg::ReRender))
|
||||
.expect_throw("failed to create history handle. Do you have a router registered?");
|
||||
|
||||
Self {
|
||||
@ -93,7 +93,7 @@ where
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let route = ctx.link().location().and_then(|m| m.route::<R>());
|
||||
let route = ctx.link().route::<R>();
|
||||
|
||||
let children = match &route {
|
||||
Some(ref route) => (ctx.props().render.0)(route),
|
||||
|
||||
@ -22,7 +22,7 @@ pub fn base_url() -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn fetch_base_url() -> Option<String> {
|
||||
match gloo_utils::document().query_selector("base[href]") {
|
||||
match gloo::utils::document().query_selector("base[href]") {
|
||||
Ok(Some(base)) => {
|
||||
let base = base.unchecked_into::<web_sys::HtmlBaseElement>().href();
|
||||
|
||||
@ -43,7 +43,7 @@ pub fn fetch_base_url() -> Option<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gloo_utils::document;
|
||||
use gloo::utils::document;
|
||||
use wasm_bindgen_test::wasm_bindgen_test as test;
|
||||
use yew_router::prelude::*;
|
||||
use yew_router::utils::*;
|
||||
|
||||
131
packages/yew-router/tests/basename.rs
Normal file
131
packages/yew-router/tests/basename.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
use yew::functional::function_component;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
mod utils;
|
||||
use utils::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Query {
|
||||
foo: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Routable)]
|
||||
enum Routes {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/no/:id")]
|
||||
No { id: u32 },
|
||||
#[at("/404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
struct NoProps {
|
||||
id: u32,
|
||||
}
|
||||
|
||||
#[function_component(No)]
|
||||
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">{ location.query::<Query>().unwrap().foo }</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(Comp)]
|
||||
fn component() -> Html {
|
||||
let navigator = use_navigator().unwrap();
|
||||
|
||||
let switch = Switch::render(move |routes| {
|
||||
let navigator_clone = navigator.clone();
|
||||
let replace_route = Callback::from(move |_| {
|
||||
navigator_clone
|
||||
.replace_with_query(
|
||||
Routes::No { id: 2 },
|
||||
Query {
|
||||
foo: "bar".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let navigator_clone = navigator.clone();
|
||||
let push_route = Callback::from(move |_| {
|
||||
navigator_clone
|
||||
.push_with_query(
|
||||
Routes::No { id: 3 },
|
||||
Query {
|
||||
foo: "baz".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
match routes {
|
||||
Routes::Home => html! {
|
||||
<>
|
||||
<div id="result">{"Home"}</div>
|
||||
<button onclick={replace_route}>{"replace a route"}</button>
|
||||
</>
|
||||
},
|
||||
Routes::No { id } => html! {
|
||||
<>
|
||||
<No id={*id} />
|
||||
<button onclick={push_route}>{"push a route"}</button>
|
||||
</>
|
||||
},
|
||||
Routes::NotFound => html! { <div id="result">{"404"}</div> },
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<Switch<Routes> render={switch} />
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(Root)]
|
||||
fn root() -> Html {
|
||||
html! {
|
||||
<BrowserRouter basename="/base/">
|
||||
<Comp />
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
|
||||
// all the tests are in place because document state isn't being reset between tests
|
||||
// different routes at the time of execution are set and it causes weird behavior (tests
|
||||
// failing randomly)
|
||||
// this test tests
|
||||
// - routing
|
||||
// - parameters in the path
|
||||
// - query parameters
|
||||
// - 404 redirects
|
||||
#[test]
|
||||
fn router_works() {
|
||||
yew::start_app_in_element::<Root>(gloo::utils::document().get_element_by_id("output").unwrap());
|
||||
|
||||
assert_eq!("Home", obtain_result_by_id("result"));
|
||||
|
||||
let initial_length = history_length();
|
||||
|
||||
click("button"); // replacing the current route
|
||||
assert_eq!("2", obtain_result_by_id("result-params"));
|
||||
assert_eq!("bar", obtain_result_by_id("result-query"));
|
||||
assert_eq!(initial_length, history_length());
|
||||
|
||||
click("button"); // pushing a new route
|
||||
assert_eq!("3", obtain_result_by_id("result-params"));
|
||||
assert_eq!("baz", obtain_result_by_id("result-query"));
|
||||
assert_eq!(initial_length + 1, history_length());
|
||||
}
|
||||
@ -45,12 +45,12 @@ fn no(props: &NoProps) -> Html {
|
||||
|
||||
#[function_component(Comp)]
|
||||
fn component() -> Html {
|
||||
let history = use_history().unwrap();
|
||||
let navigator = use_navigator().unwrap();
|
||||
|
||||
let switch = Switch::render(move |routes| {
|
||||
let history_clone = history.clone();
|
||||
let navigator_clone = navigator.clone();
|
||||
let replace_route = Callback::from(move |_| {
|
||||
history_clone
|
||||
navigator_clone
|
||||
.replace_with_query(
|
||||
Routes::No { id: 2 },
|
||||
Query {
|
||||
@ -60,9 +60,9 @@ fn component() -> Html {
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let history_clone = history.clone();
|
||||
let navigator_clone = navigator.clone();
|
||||
let push_route = Callback::from(move |_| {
|
||||
history_clone
|
||||
navigator_clone
|
||||
.push_with_query(
|
||||
Routes::No { id: 3 },
|
||||
Query {
|
||||
@ -113,7 +113,7 @@ fn root() -> Html {
|
||||
// - 404 redirects
|
||||
#[test]
|
||||
fn router_works() {
|
||||
yew::start_app_in_element::<Root>(gloo_utils::document().get_element_by_id("output").unwrap());
|
||||
yew::start_app_in_element::<Root>(gloo::utils::document().get_element_by_id("output").unwrap());
|
||||
|
||||
assert_eq!("Home", obtain_result_by_id("result"));
|
||||
|
||||
131
packages/yew-router/tests/hash_router.rs
Normal file
131
packages/yew-router/tests/hash_router.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
use yew::functional::function_component;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
mod utils;
|
||||
use utils::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Query {
|
||||
foo: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Routable)]
|
||||
enum Routes {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/no/:id")]
|
||||
No { id: u32 },
|
||||
#[at("/404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
struct NoProps {
|
||||
id: u32,
|
||||
}
|
||||
|
||||
#[function_component(No)]
|
||||
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">{ location.query::<Query>().unwrap().foo }</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(Comp)]
|
||||
fn component() -> Html {
|
||||
let navigator = use_navigator().unwrap();
|
||||
|
||||
let switch = Switch::render(move |routes| {
|
||||
let navigator_clone = navigator.clone();
|
||||
let replace_route = Callback::from(move |_| {
|
||||
navigator_clone
|
||||
.replace_with_query(
|
||||
Routes::No { id: 2 },
|
||||
Query {
|
||||
foo: "bar".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let navigator_clone = navigator.clone();
|
||||
let push_route = Callback::from(move |_| {
|
||||
navigator_clone
|
||||
.push_with_query(
|
||||
Routes::No { id: 3 },
|
||||
Query {
|
||||
foo: "baz".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
match routes {
|
||||
Routes::Home => html! {
|
||||
<>
|
||||
<div id="result">{"Home"}</div>
|
||||
<button onclick={replace_route}>{"replace a route"}</button>
|
||||
</>
|
||||
},
|
||||
Routes::No { id } => html! {
|
||||
<>
|
||||
<No id={*id} />
|
||||
<button onclick={push_route}>{"push a route"}</button>
|
||||
</>
|
||||
},
|
||||
Routes::NotFound => html! { <div id="result">{"404"}</div> },
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<Switch<Routes> render={switch} />
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(Root)]
|
||||
fn root() -> Html {
|
||||
html! {
|
||||
<HashRouter>
|
||||
<Comp />
|
||||
</HashRouter>
|
||||
}
|
||||
}
|
||||
|
||||
// all the tests are in place because document state isn't being reset between tests
|
||||
// different routes at the time of execution are set and it causes weird behavior (tests
|
||||
// failing randomly)
|
||||
// this test tests
|
||||
// - routing
|
||||
// - parameters in the path
|
||||
// - query parameters
|
||||
// - 404 redirects
|
||||
#[test]
|
||||
fn router_works() {
|
||||
yew::start_app_in_element::<Root>(gloo::utils::document().get_element_by_id("output").unwrap());
|
||||
|
||||
assert_eq!("Home", obtain_result_by_id("result"));
|
||||
|
||||
let initial_length = history_length();
|
||||
|
||||
click("button"); // replacing the current route
|
||||
assert_eq!("2", obtain_result_by_id("result-params"));
|
||||
assert_eq!("bar", obtain_result_by_id("result-query"));
|
||||
assert_eq!(initial_length, history_length());
|
||||
|
||||
click("button"); // pushing a new route
|
||||
assert_eq!("3", obtain_result_by_id("result-params"));
|
||||
assert_eq!("baz", obtain_result_by_id("result-query"));
|
||||
assert_eq!(initial_length + 1, history_length());
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
pub fn obtain_result_by_id(id: &str) -> String {
|
||||
gloo_utils::document()
|
||||
gloo::utils::document()
|
||||
.get_element_by_id(id)
|
||||
.expect("No result found. Most likely, the application crashed and burned")
|
||||
.inner_html()
|
||||
}
|
||||
|
||||
pub fn click(selector: &str) {
|
||||
gloo_utils::document()
|
||||
gloo::utils::document()
|
||||
.query_selector(selector)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
@ -18,7 +18,7 @@ pub fn click(selector: &str) {
|
||||
}
|
||||
|
||||
pub fn history_length() -> u32 {
|
||||
gloo_utils::window()
|
||||
gloo::utils::window()
|
||||
.history()
|
||||
.expect("No history found")
|
||||
.length()
|
||||
|
||||
@ -44,10 +44,13 @@ current URL and passes it to the `render` callback. The callback then decides wh
|
||||
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.
|
||||
|
||||
Finally, you need to register the `<Router />` component as a context.
|
||||
`<Router />` provides session history information to its children.
|
||||
Finally, you need to register one of the Router context provider components like `<BrowserRouter />`.
|
||||
It provides location information and navigator to its children.
|
||||
|
||||
When using `yew-router` in browser environment, `<BrowserRouter />` is recommended.
|
||||
:::caution
|
||||
When using `yew-router` in browser environment, `<BrowserRouter />` is highly recommended.
|
||||
You can find other router flavours in the [API Reference](https://docs.rs/yew-router/).
|
||||
:::
|
||||
|
||||
```rust
|
||||
use yew_router::prelude::*;
|
||||
@ -66,9 +69,9 @@ enum Route {
|
||||
|
||||
#[function_component(Secure)]
|
||||
fn secure() -> Html {
|
||||
let history = use_history().unwrap();
|
||||
let navigator = use_navigator().unwrap();
|
||||
|
||||
let onclick = Callback::once(move |_| history.push(Route::Home));
|
||||
let onclick = Callback::once(move |_| navigator.push(Route::Home));
|
||||
html! {
|
||||
<div>
|
||||
<h1>{ "Secure" }</h1>
|
||||
@ -161,28 +164,10 @@ unmatched.
|
||||
For more information about the route syntax and how to bind parameters, check
|
||||
out [route-recognizer](https://docs.rs/route-recognizer/0.3.1/route_recognizer/#routing-params).
|
||||
|
||||
### History and Location
|
||||
### 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`).
|
||||
The router provides a universal `Location` struct via context which can be used to access routing information.
|
||||
They can be retrieved by hooks or convenient functions on `ctx.link()`.
|
||||
|
||||
### Navigation
|
||||
|
||||
@ -190,12 +175,12 @@ in `AnyHistory` and
|
||||
|
||||
#### Link
|
||||
|
||||
A `<Link/>` renders as an `<a>` element, the `onclick` event handler will call
|
||||
A `<Link />` renders as an `<a>` element, the `onclick` event handler will call
|
||||
[preventDefault](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault), and push the targeted page to the
|
||||
history and render the desired page, which is what should be expected from a Single Page App. The default onclick of a
|
||||
normal anchor element would reload the page.
|
||||
|
||||
The `<Link/>` element also passes its children to the `<a>` element. Consider it a replacement of `<a/>` for in-app
|
||||
The `<Link />` component also passes its children to the `<a>` element. Consider it a replacement of `<a/>` for in-app
|
||||
routes. Except you supply a `to` attribute instead of a `href`. An example usage:
|
||||
|
||||
```rust ,ignore
|
||||
@ -208,21 +193,21 @@ Struct variants work as expected too:
|
||||
<Link<Route> to={Route::Post { id: "new-yew-release".to_string() }}>{ "Yew v0.19 out now!" }</Link<Route>>
|
||||
```
|
||||
|
||||
#### History API
|
||||
#### Navigator API
|
||||
|
||||
History API is provided for both function components and struct components. They can enable callbacks to change the
|
||||
route. An `AnyHistory` instance can be obtained in either cases to manipulate the route.
|
||||
Navigator API is provided for both function components and struct components. They enable callbacks to change the
|
||||
route. An `Navigator` instance can be obtained in either cases to manipulate the route.
|
||||
|
||||
##### Function Components
|
||||
|
||||
For function components, the `use_history` hook re-renders the component and returns the current route whenever the
|
||||
history changes. Here's how to implement a button that navigates to the `Home` route when clicked.
|
||||
For function components, the `use_navigator` hook re-renders the component when the underlying navigator provider changes.
|
||||
Here's how to implement a button that navigates to the `Home` route when clicked.
|
||||
|
||||
```rust ,ignore
|
||||
#[function_component(MyComponent)]
|
||||
pub fn my_component() -> Html {
|
||||
let history = use_history().unwrap();
|
||||
let onclick = Callback::once(move |_| history.push(Route::Home));
|
||||
let navigator = use_navigator().unwrap();
|
||||
let onclick = Callback::once(move |_| navigator.push(Route::Home));
|
||||
|
||||
html! {
|
||||
<>
|
||||
@ -236,13 +221,13 @@ pub fn my_component() -> Html {
|
||||
The example here uses `Callback::once`. Use a normal callback if the target route can be the same with the route
|
||||
the component is in, or just to play safe. For example, when you have a logo button on every page, that goes back to
|
||||
home when clicked, clicking that button twice on home page causes the code to panic because the second click pushes an
|
||||
identical Home route and the `use_history` hook won't trigger a re-render.
|
||||
identical Home route and the `use_navigator` hook won't trigger a re-render.
|
||||
:::
|
||||
|
||||
If you want to replace the current history instead of pushing a new history onto the stack, use `history.replace()`
|
||||
instead of `history.push()`.
|
||||
If you want to replace the current location instead of pushing a new location onto the stack, use `navigator.replace()`
|
||||
instead of `navigator.push()`.
|
||||
|
||||
You may notice `history` has to move into the callback, so it can't be used again for other callbacks. Luckily `history`
|
||||
You may notice `navigator` has to move into the callback, so it can't be used again for other callbacks. Luckily `navigator`
|
||||
implements `Clone`, here's for example how to have multiple buttons to different routes:
|
||||
|
||||
```rust ,ignore
|
||||
@ -251,26 +236,26 @@ use yew_router::prelude::*;
|
||||
|
||||
#[function_component(NavItems)]
|
||||
pub fn nav_items() -> Html {
|
||||
let history = use_history().unwrap();
|
||||
let navigator = use_navigator().unwrap();
|
||||
|
||||
let go_home_button = {
|
||||
let history = history.clone();
|
||||
let onclick = Callback::once(move |_| history.push(Route::Home));
|
||||
let navigator = navigator.clone();
|
||||
let onclick = Callback::once(move |_| navigator.push(Route::Home));
|
||||
html! {
|
||||
<button {onclick}>{"click to go home"}</button>
|
||||
}
|
||||
};
|
||||
|
||||
let go_to_first_post_button = {
|
||||
let history = history.clone();
|
||||
let onclick = Callback::once(move |_| history.push(Route::Post { id: "first-post".to_string() }));
|
||||
let navigator = navigator.clone();
|
||||
let onclick = Callback::once(move |_| navigator.push(Route::Post { id: "first-post".to_string() }));
|
||||
html! {
|
||||
<button {onclick}>{"click to go the first post"}</button>
|
||||
}
|
||||
};
|
||||
|
||||
let go_to_secure_button = {
|
||||
let onclick = Callback::once(move |_| history.push(Route::Secure));
|
||||
let onclick = Callback::once(move |_| navigator.push(Route::Secure));
|
||||
html! {
|
||||
<button {onclick}>{"click to go to secure"}</button>
|
||||
}
|
||||
@ -286,22 +271,15 @@ pub fn nav_items() -> Html {
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
This is a hack and a more idiomatic hook version will come in the future!
|
||||
But if your component only needs to set the route without listening to the changes, instead of the `use_history`
|
||||
hook, `BrowserHistory::default()` can be used to acquire the global history instance. The latter also works in non-threaded agent
|
||||
environments (`Context` and `Job`).
|
||||
:::
|
||||
|
||||
##### Struct Components
|
||||
|
||||
For struct components, the `AnyHistory` instance can be obtained through the `ctx.link().history()` API. The rest is
|
||||
For struct components, the `Navigator` instance can be obtained through the `ctx.link().navigator()` API. The rest is
|
||||
identical with the function component case. Here's an example of a view function that renders a single button.
|
||||
|
||||
```rust ,ignore
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let history = ctx.link().history().unwrap();
|
||||
let onclick = Callback::once(move |_| history.push(MainRoute::Home));
|
||||
let navigator = ctx.link().navigator().unwrap();
|
||||
let onclick = Callback::once(move |_| navigator.push(MainRoute::Home));
|
||||
html!{
|
||||
<button {onclick}>{"Go Home"}</button>
|
||||
}
|
||||
@ -310,10 +288,10 @@ fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
|
||||
#### Redirect
|
||||
|
||||
`yew-router` also provides a `<Redirect/>` element in the prelude. It can be used to achieve similar effects as the
|
||||
history API. The element accepts a
|
||||
`to` attribute as the target route. When a `<Redirect/>` element is rendered, it internally calls `history.push()` and
|
||||
changes the route. Here is an example:
|
||||
`yew-router` also provides a `<Redirect />` component in the prelude. It can be used to achieve similar effects as the
|
||||
navigator API. The component accepts a
|
||||
`to` attribute as the target route. When a `<Redirect/>` is rendered users will be redirect to the route specified in props.
|
||||
Here is an example:
|
||||
|
||||
```rust ,ignore
|
||||
#[function_component(SomePage)]
|
||||
@ -321,9 +299,7 @@ fn some_page() -> Html {
|
||||
// made-up hook `use_user`
|
||||
let user = match use_user() {
|
||||
Some(user) => user,
|
||||
// an early return that redirects to the login page
|
||||
// technicality: `Redirect` actually renders an empty html. But since it also pushes history, the target page
|
||||
// shows up immediately. Consider it a "side-effect" component.
|
||||
// Redirects to the login page when user is `None`.
|
||||
None => return html! {
|
||||
<Redirect<Route> to={Route::Login}/>
|
||||
},
|
||||
@ -332,9 +308,9 @@ fn some_page() -> Html {
|
||||
}
|
||||
```
|
||||
|
||||
:::tip `Redirect` vs `history`, which to use
|
||||
The history API is the only way to manipulate route in callbacks.
|
||||
While `<Redirect/>` can be used as return values in a component. You might also want to use `<Redirect/>` in other
|
||||
:::tip `Redirect` vs `Navigator`, which to use
|
||||
The Navigator API is the only way to manipulate route in callbacks.
|
||||
While `<Redirect />` can be used as return values in a component. You might also want to use `<Redirect />` in other
|
||||
non-component context, for example in the switch function of a [Nested Router](#nested-router).
|
||||
:::
|
||||
|
||||
@ -342,22 +318,22 @@ non-component context, for example in the switch function of a [Nested Router](#
|
||||
|
||||
#### Function Components
|
||||
|
||||
Alongside the `use_history` hook, there are also `use_location` and `use_route`. Your components will re-render when
|
||||
You can use `use_location` and `use_route` hooks. Your components will re-render when
|
||||
provided values change.
|
||||
|
||||
#### Struct Components
|
||||
|
||||
In order to react on route changes, you can pass a callback closure to the `add_history_listener()` method of `ctx.link()`.
|
||||
In order to react on route changes, you can pass a callback closure to the `add_location_listener()` method of `ctx.link()`.
|
||||
|
||||
:::note
|
||||
The history listener will get unregistered once it is dropped. Make sure to store the handle inside your
|
||||
The location listener will get unregistered once it is dropped. Make sure to store the handle inside your
|
||||
component state.
|
||||
:::
|
||||
|
||||
```rust ,ignore
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let listener = ctx.link()
|
||||
.add_history_listener(ctx.link().callback(
|
||||
.add_location_listener(ctx.link().callback(
|
||||
// handle event
|
||||
))
|
||||
.unwrap();
|
||||
@ -373,8 +349,8 @@ fn create(ctx: &Context<Self>) -> Self {
|
||||
|
||||
#### Specifying query parameters when navigating
|
||||
|
||||
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
|
||||
In order to specify query parameters when navigating to a new route, use either `navigator.push_with_query` or
|
||||
the `navigator.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.
|
||||
|
||||
@ -477,6 +453,22 @@ pub fn app() -> Html {
|
||||
}
|
||||
```
|
||||
|
||||
### Basename
|
||||
|
||||
It's possible to define a basename with `yew-router`.
|
||||
A basename is a common prefix of all routes. Both the Navigator API and
|
||||
`<Switch />` component respect basename setting. All pushed routes will be
|
||||
prefixed with the basename and all switches will strip the basename before
|
||||
trying to parse the path into a `Routable`.
|
||||
|
||||
If a basename prop is not supplied to the Router component, it will use
|
||||
the href attribute of the `<base />` element in your html file and
|
||||
fallback to `/` if no `<base />` presents in the html file.
|
||||
|
||||
## Relevant examples
|
||||
|
||||
- [Router](https://github.com/yewstack/yew/tree/master/examples/router)
|
||||
|
||||
## API Reference
|
||||
|
||||
- [yew-router](https://docs.rs/yew-router/)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user