mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Make Switch to accept a closure as render function directly (#2659)
* RenderFn can be be directly Fn() now. * Switch switch to function component. * Make Link a function component. * Remove non-exhaustive. * Add migration guide. * Update Website Docs. * Fix CI. * Pushing to Navigator no longer requires an owned instance. * Fix CI. * Fix code size. * Further optimisation.
This commit is contained in:
parent
27da544cae
commit
00b94d0051
@ -37,7 +37,7 @@ pub fn App() -> Html {
|
|||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Switch<Route> render={Switch::render(switch)} />
|
<Switch<Route> render={switch} />
|
||||||
</main>
|
</main>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
@ -71,7 +71,7 @@ pub fn ServerApp(props: &ServerAppProps) -> Html {
|
|||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Switch<Route> render={Switch::render(switch)} />
|
<Switch<Route> render={switch} />
|
||||||
</main>
|
</main>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
@ -87,8 +87,8 @@ pub fn ServerApp(props: &ServerAppProps) -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch(routes: &Route) -> Html {
|
fn switch(routes: Route) -> Html {
|
||||||
match routes.clone() {
|
match routes {
|
||||||
Route::Post { id } => {
|
Route::Post { id } => {
|
||||||
html! { <Post seed={id} /> }
|
html! { <Post seed={id} /> }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,7 +62,7 @@ impl Component for App {
|
|||||||
{ self.view_nav(ctx.link()) }
|
{ self.view_nav(ctx.link()) }
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Switch<Route> render={Switch::render(switch)} />
|
<Switch<Route> render={switch} />
|
||||||
</main>
|
</main>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
@ -124,8 +124,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch(routes: &Route) -> Html {
|
fn switch(routes: Route) -> Html {
|
||||||
match routes.clone() {
|
match routes {
|
||||||
Route::Post { id } => {
|
Route::Post { id } => {
|
||||||
html! { <Post seed={id} /> }
|
html! { <Post seed={id} /> }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use wasm_bindgen::UnwrapThrowExt;
|
use wasm_bindgen::UnwrapThrowExt;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew::virtual_dom::AttrValue;
|
use yew::virtual_dom::AttrValue;
|
||||||
|
|
||||||
use crate::navigator::NavigatorKind;
|
use crate::navigator::NavigatorKind;
|
||||||
use crate::scope_ext::RouterScopeExt;
|
use crate::prelude::*;
|
||||||
use crate::{utils, Routable};
|
use crate::{utils, Routable};
|
||||||
|
|
||||||
/// Props for [`Link`]
|
/// Props for [`Link`]
|
||||||
@ -31,96 +29,65 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper around `<a>` tag to be used with [`Router`](crate::Router)
|
/// A wrapper around `<a>` tag to be used with [`Router`](crate::Router)
|
||||||
pub struct Link<R, Q = ()>
|
#[function_component]
|
||||||
|
pub fn Link<R, Q = ()>(props: &LinkProps<R, Q>) -> Html
|
||||||
where
|
where
|
||||||
R: Routable + 'static,
|
R: Routable + 'static,
|
||||||
Q: Clone + PartialEq + Serialize + 'static,
|
Q: Clone + PartialEq + Serialize + 'static,
|
||||||
{
|
{
|
||||||
_route: PhantomData<R>,
|
let LinkProps {
|
||||||
_query: PhantomData<Q>,
|
classes,
|
||||||
}
|
to,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
query,
|
||||||
|
} = props.clone();
|
||||||
|
|
||||||
pub enum Msg {
|
let navigator = use_navigator().expect_throw("failed to get navigator");
|
||||||
OnClick,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R, Q> Component for Link<R, Q>
|
let onclick = {
|
||||||
where
|
let navigator = navigator.clone();
|
||||||
R: Routable + 'static,
|
let to = to.clone();
|
||||||
Q: Clone + PartialEq + Serialize + 'static,
|
let query = query.clone();
|
||||||
{
|
|
||||||
type Message = Msg;
|
|
||||||
type Properties = LinkProps<R, Q>;
|
|
||||||
|
|
||||||
fn create(_ctx: &Context<Self>) -> Self {
|
Callback::from(move |e: MouseEvent| {
|
||||||
Self {
|
|
||||||
_route: PhantomData,
|
|
||||||
_query: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
||||||
match msg {
|
|
||||||
Msg::OnClick => {
|
|
||||||
let LinkProps { to, query, .. } = ctx.props();
|
|
||||||
let navigator = ctx
|
|
||||||
.link()
|
|
||||||
.navigator()
|
|
||||||
.expect_throw("failed to get navigator");
|
|
||||||
match query {
|
|
||||||
None => {
|
|
||||||
navigator.push(to.clone());
|
|
||||||
}
|
|
||||||
Some(data) => {
|
|
||||||
navigator
|
|
||||||
.push_with_query(to.clone(), data.clone())
|
|
||||||
.expect_throw("failed push history with query");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
||||||
let LinkProps {
|
|
||||||
classes,
|
|
||||||
to,
|
|
||||||
query,
|
|
||||||
children,
|
|
||||||
disabled,
|
|
||||||
..
|
|
||||||
} = ctx.props().clone();
|
|
||||||
let onclick = ctx.link().callback(|e: MouseEvent| {
|
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
Msg::OnClick
|
|
||||||
});
|
|
||||||
|
|
||||||
let navigator = ctx
|
match query {
|
||||||
.link()
|
None => {
|
||||||
.navigator()
|
navigator.push(&to);
|
||||||
.expect_throw("failed to get navigator");
|
}
|
||||||
let href: AttrValue = {
|
Some(ref data) => {
|
||||||
let pathname = navigator.route_to_url(to);
|
navigator
|
||||||
let path = query
|
.push_with_query(&to, data)
|
||||||
.and_then(|query| serde_urlencoded::to_string(query).ok())
|
.expect_throw("failed push history with query");
|
||||||
.and_then(|query| utils::compose_path(&pathname, &query))
|
}
|
||||||
.unwrap_or_else(|| pathname.to_string());
|
|
||||||
|
|
||||||
match navigator.kind() {
|
|
||||||
NavigatorKind::Hash => format!("#{}", path),
|
|
||||||
_ => path,
|
|
||||||
}
|
}
|
||||||
.into()
|
})
|
||||||
};
|
};
|
||||||
html! {
|
|
||||||
<a class={classes}
|
let href = {
|
||||||
{href}
|
let route_s = to.to_path();
|
||||||
{onclick}
|
let pathname = navigator.prefix_basename(&route_s);
|
||||||
{disabled}
|
let mut path = query
|
||||||
>
|
.and_then(|query| serde_urlencoded::to_string(query).ok())
|
||||||
{ children }
|
.and_then(|query| utils::compose_path(&pathname, &query))
|
||||||
</a>
|
.unwrap_or_else(|| pathname.into_owned());
|
||||||
|
|
||||||
|
if navigator.kind() == NavigatorKind::Hash {
|
||||||
|
path.insert(0, '#');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AttrValue::from(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<a class={classes}
|
||||||
|
{href}
|
||||||
|
{onclick}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ where
|
|||||||
|
|
||||||
let target_route = props.to.clone();
|
let target_route = props.to.clone();
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
history.push(target_route.clone());
|
history.push(&target_route);
|
||||||
|
|
||||||
|| {}
|
|| {}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
//! fn secure() -> Html {
|
//! fn secure() -> Html {
|
||||||
//! let navigator = use_navigator().unwrap();
|
//! let navigator = use_navigator().unwrap();
|
||||||
//!
|
//!
|
||||||
//! let onclick_callback = Callback::from(move |_| navigator.push(Route::Home));
|
//! let onclick_callback = Callback::from(move |_| navigator.push(&Route::Home));
|
||||||
//! html! {
|
//! html! {
|
||||||
//! <div>
|
//! <div>
|
||||||
//! <h1>{ "Secure" }</h1>
|
//! <h1>{ "Secure" }</h1>
|
||||||
@ -36,12 +36,12 @@
|
|||||||
//! fn app() -> Html {
|
//! fn app() -> Html {
|
||||||
//! html! {
|
//! html! {
|
||||||
//! <BrowserRouter>
|
//! <BrowserRouter>
|
||||||
//! <Switch<Route> render={Switch::render(switch)} />
|
//! <Switch<Route> render={switch} />
|
||||||
//! </BrowserRouter>
|
//! </BrowserRouter>
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! fn switch(routes: &Route) -> Html {
|
//! fn switch(routes: Route) -> Html {
|
||||||
//! match routes {
|
//! match routes {
|
||||||
//! Route::Home => html! { <h1>{ "Home" }</h1> },
|
//! Route::Home => html! { <h1>{ "Home" }</h1> },
|
||||||
//! Route::Secure => html! {
|
//! Route::Secure => html! {
|
||||||
@ -79,7 +79,7 @@ pub mod utils;
|
|||||||
|
|
||||||
pub use routable::{AnyRoute, Routable};
|
pub use routable::{AnyRoute, Routable};
|
||||||
pub use router::{BrowserRouter, HashRouter, Router};
|
pub use router::{BrowserRouter, HashRouter, Router};
|
||||||
pub use switch::{RenderFn, Switch};
|
pub use switch::Switch;
|
||||||
|
|
||||||
pub mod history {
|
pub mod history {
|
||||||
//! A module that provides universal session history and location information.
|
//! A module that provides universal session history and location information.
|
||||||
|
|||||||
@ -57,77 +57,94 @@ impl Navigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes a [`Routable`] entry.
|
/// Pushes a [`Routable`] entry.
|
||||||
pub fn push(&self, route: impl Routable) {
|
pub fn push<R>(&self, route: &R)
|
||||||
self.inner.push(self.route_to_url(route));
|
where
|
||||||
|
R: Routable,
|
||||||
|
{
|
||||||
|
self.inner.push(self.prefix_basename(&route.to_path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces the current history entry with provided [`Routable`] and [`None`] state.
|
/// Replaces the current history entry with provided [`Routable`] and [`None`] state.
|
||||||
pub fn replace(&self, route: impl Routable) {
|
pub fn replace<R>(&self, route: &R)
|
||||||
self.inner.replace(self.route_to_url(route));
|
where
|
||||||
|
R: Routable,
|
||||||
|
{
|
||||||
|
self.inner.replace(self.prefix_basename(&route.to_path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes a [`Routable`] entry with state.
|
/// Pushes a [`Routable`] entry with state.
|
||||||
pub fn push_with_state<T>(&self, route: impl Routable, state: T)
|
pub fn push_with_state<R, T>(&self, route: &R, state: T)
|
||||||
where
|
where
|
||||||
|
R: Routable,
|
||||||
T: 'static,
|
T: 'static,
|
||||||
{
|
{
|
||||||
self.inner.push_with_state(self.route_to_url(route), state);
|
self.inner
|
||||||
|
.push_with_state(self.prefix_basename(&route.to_path()), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces the current history entry with provided [`Routable`] and state.
|
/// Replaces the current history entry with provided [`Routable`] and state.
|
||||||
pub fn replace_with_state<T>(&self, route: impl Routable, state: T)
|
pub fn replace_with_state<R, T>(&self, route: &R, state: T)
|
||||||
where
|
where
|
||||||
|
R: Routable,
|
||||||
T: 'static,
|
T: 'static,
|
||||||
{
|
{
|
||||||
self.inner
|
self.inner
|
||||||
.replace_with_state(self.route_to_url(route), state);
|
.replace_with_state(self.prefix_basename(&route.to_path()), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `.push()` but affix the queries to the end of the route.
|
/// 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<()>
|
pub fn push_with_query<R, Q>(&self, route: &R, query: &Q) -> NavigationResult<()>
|
||||||
where
|
where
|
||||||
|
R: Routable,
|
||||||
Q: Serialize,
|
Q: Serialize,
|
||||||
{
|
{
|
||||||
self.inner.push_with_query(self.route_to_url(route), query)
|
self.inner
|
||||||
|
.push_with_query(self.prefix_basename(&route.to_path()), query)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `.replace()` but affix the queries to the end of the route.
|
/// 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<()>
|
pub fn replace_with_query<R, Q>(&self, route: &R, query: &Q) -> NavigationResult<()>
|
||||||
where
|
where
|
||||||
|
R: Routable,
|
||||||
Q: Serialize,
|
Q: Serialize,
|
||||||
{
|
{
|
||||||
self.inner
|
self.inner
|
||||||
.replace_with_query(self.route_to_url(route), query)
|
.replace_with_query(self.prefix_basename(&route.to_path()), query)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `.push_with_state()` but affix the queries to the end of the route.
|
/// Same as `.push_with_state()` but affix the queries to the end of the route.
|
||||||
pub fn push_with_query_and_state<Q, T>(
|
pub fn push_with_query_and_state<R, Q, T>(
|
||||||
&self,
|
&self,
|
||||||
route: impl Routable,
|
route: &R,
|
||||||
query: Q,
|
query: &Q,
|
||||||
state: T,
|
state: T,
|
||||||
) -> NavigationResult<()>
|
) -> NavigationResult<()>
|
||||||
where
|
where
|
||||||
|
R: Routable,
|
||||||
Q: Serialize,
|
Q: Serialize,
|
||||||
T: 'static,
|
T: 'static,
|
||||||
{
|
{
|
||||||
self.inner
|
self.inner
|
||||||
.push_with_query_and_state(self.route_to_url(route), query, state)
|
.push_with_query_and_state(self.prefix_basename(&route.to_path()), query, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `.replace_with_state()` but affix the queries to the end of the route.
|
/// Same as `.replace_with_state()` but affix the queries to the end of the route.
|
||||||
pub fn replace_with_query_and_state<Q, T>(
|
pub fn replace_with_query_and_state<R, Q, T>(
|
||||||
&self,
|
&self,
|
||||||
route: impl Routable,
|
route: &R,
|
||||||
query: Q,
|
query: &Q,
|
||||||
state: T,
|
state: T,
|
||||||
) -> NavigationResult<()>
|
) -> NavigationResult<()>
|
||||||
where
|
where
|
||||||
|
R: Routable,
|
||||||
Q: Serialize,
|
Q: Serialize,
|
||||||
T: 'static,
|
T: 'static,
|
||||||
{
|
{
|
||||||
self.inner
|
self.inner.replace_with_query_and_state(
|
||||||
.replace_with_query_and_state(self.route_to_url(route), query, state)
|
self.prefix_basename(&route.to_path()),
|
||||||
|
query,
|
||||||
|
state,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Navigator kind.
|
/// Returns the Navigator kind.
|
||||||
@ -139,22 +156,17 @@ impl Navigator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn route_to_url(&self, route: impl Routable) -> Cow<'static, str> {
|
pub(crate) fn prefix_basename<'a>(&self, route_s: &'a str) -> Cow<'a, str> {
|
||||||
let url = route.to_path();
|
match self.basename() {
|
||||||
|
|
||||||
let path = match self.basename() {
|
|
||||||
Some(base) => {
|
Some(base) => {
|
||||||
let path = format!("{}{}", base, url);
|
if route_s.is_empty() && route_s.is_empty() {
|
||||||
if path.is_empty() {
|
|
||||||
Cow::from("/")
|
Cow::from("/")
|
||||||
} else {
|
} else {
|
||||||
path.into()
|
Cow::from(format!("{}{}", base, route_s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => url.into(),
|
None => route_s.into(),
|
||||||
};
|
}
|
||||||
|
|
||||||
path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> {
|
pub(crate) fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> {
|
||||||
|
|||||||
@ -30,12 +30,16 @@ pub struct NavigatorHandle {
|
|||||||
/// # use wasm_bindgen::UnwrapThrowExt;
|
/// # use wasm_bindgen::UnwrapThrowExt;
|
||||||
/// # use yew::prelude::*;
|
/// # use yew::prelude::*;
|
||||||
/// # use yew_router::prelude::*;
|
/// # use yew_router::prelude::*;
|
||||||
/// # use yew_router::components::{LinkProps, Msg};
|
/// # use yew_router::components::LinkProps;
|
||||||
/// #
|
/// #
|
||||||
/// # pub struct Link<R: Routable + 'static> {
|
/// # pub struct Link<R: Routable + 'static> {
|
||||||
/// # _data: PhantomData<R>,
|
/// # _data: PhantomData<R>,
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
/// #
|
||||||
|
/// # pub enum Msg {
|
||||||
|
/// # OnClick,
|
||||||
|
/// # }
|
||||||
|
/// #
|
||||||
/// impl<R: Routable + 'static> Component for Link<R> {
|
/// impl<R: Routable + 'static> Component for Link<R> {
|
||||||
/// type Message = Msg;
|
/// type Message = Msg;
|
||||||
/// type Properties = LinkProps<R>;
|
/// type Properties = LinkProps<R>;
|
||||||
@ -50,7 +54,7 @@ pub struct NavigatorHandle {
|
|||||||
/// ctx.link()
|
/// ctx.link()
|
||||||
/// .navigator()
|
/// .navigator()
|
||||||
/// .expect_throw("failed to get navigator.")
|
/// .expect_throw("failed to get navigator.")
|
||||||
/// .push(ctx.props().to.clone());
|
/// .push(&ctx.props().to);
|
||||||
/// false
|
/// false
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
|||||||
@ -1,44 +1,9 @@
|
|||||||
//! The [`Switch`] Component.
|
//! The [`Switch`] Component.
|
||||||
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gloo::console;
|
use gloo::console;
|
||||||
use wasm_bindgen::UnwrapThrowExt;
|
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
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>);
|
|
||||||
|
|
||||||
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`]
|
/// Props for [`Switch`]
|
||||||
#[derive(Properties, PartialEq, Clone)]
|
#[derive(Properties, PartialEq, Clone)]
|
||||||
@ -47,16 +12,11 @@ where
|
|||||||
R: Routable,
|
R: Routable,
|
||||||
{
|
{
|
||||||
/// Callback which returns [`Html`] to be rendered for the current route.
|
/// Callback which returns [`Html`] to be rendered for the current route.
|
||||||
pub render: RenderFn<R>,
|
pub render: Callback<R, Html>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub pathname: Option<String>,
|
pub pathname: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub enum Msg {
|
|
||||||
ReRender,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Switch that dispatches route among variants of a [`Routable`].
|
/// A Switch that dispatches route among variants of a [`Routable`].
|
||||||
///
|
///
|
||||||
/// When a route can't be matched, including when the path is matched but the deserialization fails,
|
/// When a route can't be matched, including when the path is matched but the deserialization fails,
|
||||||
@ -65,65 +25,24 @@ pub enum Msg {
|
|||||||
/// Otherwise `html! {}` is rendered and a message is logged to console
|
/// Otherwise `html! {}` is rendered and a message is logged to console
|
||||||
/// stating that no route can be matched.
|
/// stating that no route can be matched.
|
||||||
/// See the [crate level document][crate] for more information.
|
/// See the [crate level document][crate] for more information.
|
||||||
pub struct Switch<R: Routable + 'static> {
|
#[function_component]
|
||||||
_listener: LocationHandle,
|
pub fn Switch<R>(props: &SwitchProps<R>) -> Html
|
||||||
_phantom: PhantomData<R>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R> Component for Switch<R>
|
|
||||||
where
|
where
|
||||||
R: Routable + 'static,
|
R: Routable + 'static,
|
||||||
{
|
{
|
||||||
type Message = Msg;
|
let route = use_route::<R>();
|
||||||
type Properties = SwitchProps<R>;
|
|
||||||
|
|
||||||
fn create(ctx: &Context<Self>) -> Self {
|
let route = props
|
||||||
let link = ctx.link();
|
.pathname
|
||||||
let listener = link
|
.as_ref()
|
||||||
.add_location_listener(link.callback(move |_| Msg::ReRender))
|
.and_then(|p| R::recognize(p))
|
||||||
.expect_throw("failed to create history handle. Do you have a router registered?");
|
.or(route);
|
||||||
|
|
||||||
Self {
|
match route {
|
||||||
_listener: listener,
|
Some(route) => props.render.emit(route),
|
||||||
_phantom: PhantomData,
|
None => {
|
||||||
|
console::warn!("no route matched");
|
||||||
|
Html::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
.props()
|
|
||||||
.pathname
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|p| R::recognize(p))
|
|
||||||
.or_else(|| ctx.link().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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,13 +50,13 @@ fn no(props: &NoProps) -> Html {
|
|||||||
fn component() -> Html {
|
fn component() -> Html {
|
||||||
let navigator = use_navigator().unwrap();
|
let navigator = use_navigator().unwrap();
|
||||||
|
|
||||||
let switch = Switch::render(move |routes| {
|
let switch = move |routes| {
|
||||||
let navigator_clone = navigator.clone();
|
let navigator_clone = navigator.clone();
|
||||||
let replace_route = Callback::from(move |_| {
|
let replace_route = Callback::from(move |_| {
|
||||||
navigator_clone
|
navigator_clone
|
||||||
.replace_with_query(
|
.replace_with_query(
|
||||||
Routes::No { id: 2 },
|
&Routes::No { id: 2 },
|
||||||
Query {
|
&Query {
|
||||||
foo: "bar".to_string(),
|
foo: "bar".to_string(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -67,8 +67,8 @@ fn component() -> Html {
|
|||||||
let push_route = Callback::from(move |_| {
|
let push_route = Callback::from(move |_| {
|
||||||
navigator_clone
|
navigator_clone
|
||||||
.push_with_query(
|
.push_with_query(
|
||||||
Routes::No { id: 3 },
|
&Routes::No { id: 3 },
|
||||||
Query {
|
&Query {
|
||||||
foo: "baz".to_string(),
|
foo: "baz".to_string(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -84,13 +84,13 @@ fn component() -> Html {
|
|||||||
},
|
},
|
||||||
Routes::No { id } => html! {
|
Routes::No { id } => html! {
|
||||||
<>
|
<>
|
||||||
<No id={*id} />
|
<No id={id} />
|
||||||
<button onclick={push_route}>{"push a route"}</button>
|
<button onclick={push_route}>{"push a route"}</button>
|
||||||
</>
|
</>
|
||||||
},
|
},
|
||||||
Routes::NotFound => html! { <div id="result">{"404"}</div> },
|
Routes::NotFound => html! { <div id="result">{"404"}</div> },
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<Switch<Routes> render={switch} />
|
<Switch<Routes> render={switch} />
|
||||||
|
|||||||
@ -50,13 +50,13 @@ fn no(props: &NoProps) -> Html {
|
|||||||
fn component() -> Html {
|
fn component() -> Html {
|
||||||
let navigator = use_navigator().unwrap();
|
let navigator = use_navigator().unwrap();
|
||||||
|
|
||||||
let switch = Switch::render(move |routes| {
|
let switch = move |routes| {
|
||||||
let navigator_clone = navigator.clone();
|
let navigator_clone = navigator.clone();
|
||||||
let replace_route = Callback::from(move |_| {
|
let replace_route = Callback::from(move |_| {
|
||||||
navigator_clone
|
navigator_clone
|
||||||
.replace_with_query(
|
.replace_with_query(
|
||||||
Routes::No { id: 2 },
|
&Routes::No { id: 2 },
|
||||||
Query {
|
&Query {
|
||||||
foo: "bar".to_string(),
|
foo: "bar".to_string(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -67,8 +67,8 @@ fn component() -> Html {
|
|||||||
let push_route = Callback::from(move |_| {
|
let push_route = Callback::from(move |_| {
|
||||||
navigator_clone
|
navigator_clone
|
||||||
.push_with_query(
|
.push_with_query(
|
||||||
Routes::No { id: 3 },
|
&Routes::No { id: 3 },
|
||||||
Query {
|
&Query {
|
||||||
foo: "baz".to_string(),
|
foo: "baz".to_string(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -84,13 +84,13 @@ fn component() -> Html {
|
|||||||
},
|
},
|
||||||
Routes::No { id } => html! {
|
Routes::No { id } => html! {
|
||||||
<>
|
<>
|
||||||
<No id={*id} />
|
<No id={id} />
|
||||||
<button onclick={push_route}>{"push a route"}</button>
|
<button onclick={push_route}>{"push a route"}</button>
|
||||||
</>
|
</>
|
||||||
},
|
},
|
||||||
Routes::NotFound => html! { <div id="result">{"404"}</div> },
|
Routes::NotFound => html! { <div id="result">{"404"}</div> },
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<Switch<Routes> render={switch} />
|
<Switch<Routes> render={switch} />
|
||||||
|
|||||||
@ -50,13 +50,13 @@ fn no(props: &NoProps) -> Html {
|
|||||||
fn component() -> Html {
|
fn component() -> Html {
|
||||||
let navigator = use_navigator().unwrap();
|
let navigator = use_navigator().unwrap();
|
||||||
|
|
||||||
let switch = Switch::render(move |routes| {
|
let switch = move |routes| {
|
||||||
let navigator_clone = navigator.clone();
|
let navigator_clone = navigator.clone();
|
||||||
let replace_route = Callback::from(move |_| {
|
let replace_route = Callback::from(move |_| {
|
||||||
navigator_clone
|
navigator_clone
|
||||||
.replace_with_query(
|
.replace_with_query(
|
||||||
Routes::No { id: 2 },
|
&Routes::No { id: 2 },
|
||||||
Query {
|
&Query {
|
||||||
foo: "bar".to_string(),
|
foo: "bar".to_string(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -67,8 +67,8 @@ fn component() -> Html {
|
|||||||
let push_route = Callback::from(move |_| {
|
let push_route = Callback::from(move |_| {
|
||||||
navigator_clone
|
navigator_clone
|
||||||
.push_with_query(
|
.push_with_query(
|
||||||
Routes::No { id: 3 },
|
&Routes::No { id: 3 },
|
||||||
Query {
|
&Query {
|
||||||
foo: "baz".to_string(),
|
foo: "baz".to_string(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -84,13 +84,13 @@ fn component() -> Html {
|
|||||||
},
|
},
|
||||||
Routes::No { id } => html! {
|
Routes::No { id } => html! {
|
||||||
<>
|
<>
|
||||||
<No id={*id} />
|
<No id={id} />
|
||||||
<button onclick={push_route}>{"push a route"}</button>
|
<button onclick={push_route}>{"push a route"}</button>
|
||||||
</>
|
</>
|
||||||
},
|
},
|
||||||
Routes::NotFound => html! { <div id="result">{"404"}</div> },
|
Routes::NotFound => html! { <div id="result">{"404"}</div> },
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<Switch<Routes> render={switch} />
|
<Switch<Routes> render={switch} />
|
||||||
|
|||||||
@ -16,7 +16,7 @@ pub use use_reducer::*;
|
|||||||
pub use use_ref::*;
|
pub use use_ref::*;
|
||||||
pub use use_state::*;
|
pub use use_state::*;
|
||||||
|
|
||||||
use crate::functional::{AnyScope, HookContext};
|
use crate::functional::HookContext;
|
||||||
|
|
||||||
/// A trait that is implemented on hooks.
|
/// A trait that is implemented on hooks.
|
||||||
///
|
///
|
||||||
@ -52,17 +52,3 @@ impl<T> Hook for BoxedHook<'_, T> {
|
|||||||
(self.inner)(ctx)
|
(self.inner)(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn use_component_scope() -> impl Hook<Output = AnyScope> {
|
|
||||||
struct HookProvider {}
|
|
||||||
|
|
||||||
impl Hook for HookProvider {
|
|
||||||
type Output = AnyScope;
|
|
||||||
|
|
||||||
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
|
||||||
ctx.scope.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HookProvider {}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::callback::Callback;
|
use crate::callback::Callback;
|
||||||
use crate::context::ContextHandle;
|
use crate::context::ContextHandle;
|
||||||
use crate::functional::{hook, use_component_scope, use_memo, use_state};
|
use crate::functional::{Hook, HookContext};
|
||||||
|
|
||||||
/// Hook for consuming context values in function components.
|
/// Hook for consuming context values in function components.
|
||||||
/// The context of the type passed as `T` is returned. If there is no such context in scope, `None`
|
/// The context of the type passed as `T` is returned. If there is no such context in scope, `None`
|
||||||
@ -65,29 +69,53 @@ use crate::functional::{hook, use_component_scope, use_memo, use_state};
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[hook]
|
pub fn use_context<T: Clone + PartialEq + 'static>() -> impl Hook<Output = Option<T>> {
|
||||||
pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> {
|
struct HookProvider<T: Clone + PartialEq + 'static> {
|
||||||
struct UseContext<T: Clone + PartialEq + 'static> {
|
_marker: PhantomData<T>,
|
||||||
context: Option<(T, ContextHandle<T>)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let scope = use_component_scope();
|
struct UseContext<T: Clone + PartialEq + 'static> {
|
||||||
|
_handle: Option<ContextHandle<T>>,
|
||||||
|
value: Rc<RefCell<Option<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
let val = use_state(|| -> Option<T> { None });
|
impl<T> Hook for HookProvider<T>
|
||||||
let state = {
|
where
|
||||||
let val_dispatcher = val.setter();
|
T: Clone + PartialEq + 'static,
|
||||||
use_memo(
|
{
|
||||||
move |_| UseContext {
|
type Output = Option<T>;
|
||||||
context: scope.context::<T>(Callback::from(move |m| {
|
|
||||||
val_dispatcher.clone().set(Some(m));
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// we fallback to initial value if it was not updated.
|
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
||||||
(*val)
|
let scope = ctx.scope.clone();
|
||||||
.clone()
|
|
||||||
.or_else(move || state.context.as_ref().map(|m| m.0.clone()))
|
let state = ctx.next_state(move |re_render| -> UseContext<T> {
|
||||||
|
let value_cell: Rc<RefCell<Option<T>>> = Rc::default();
|
||||||
|
|
||||||
|
let (init_value, handle) = {
|
||||||
|
let value_cell = value_cell.clone();
|
||||||
|
|
||||||
|
scope.context(Callback::from(move |m| {
|
||||||
|
*(value_cell.borrow_mut()) = Some(m);
|
||||||
|
re_render()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
.map(|(value, handle)| (Some(value), Some(handle)))
|
||||||
|
.unwrap_or((None, None));
|
||||||
|
|
||||||
|
*(value_cell.borrow_mut()) = init_value;
|
||||||
|
|
||||||
|
UseContext {
|
||||||
|
_handle: handle,
|
||||||
|
value: value_cell,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let value = state.value.borrow();
|
||||||
|
value.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HookProvider {
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#![cfg(feature = "hydration")]
|
#![cfg(feature = "hydration")]
|
||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|||||||
@ -71,7 +71,7 @@ enum Route {
|
|||||||
fn secure() -> Html {
|
fn secure() -> Html {
|
||||||
let navigator = use_navigator().unwrap();
|
let navigator = use_navigator().unwrap();
|
||||||
|
|
||||||
let onclick = Callback::from(move |_| navigator.push(Route::Home));
|
let onclick = Callback::from(move |_| navigator.push(&Route::Home));
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
<h1>{ "Secure" }</h1>
|
<h1>{ "Secure" }</h1>
|
||||||
@ -80,7 +80,7 @@ fn secure() -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch(routes: &Route) -> Html {
|
fn switch(routes: Route) -> Html {
|
||||||
match routes {
|
match routes {
|
||||||
Route::Home => html! { <h1>{ "Home" }</h1> },
|
Route::Home => html! { <h1>{ "Home" }</h1> },
|
||||||
Route::Secure => html! {
|
Route::Secure => html! {
|
||||||
@ -94,7 +94,7 @@ fn switch(routes: &Route) -> Html {
|
|||||||
fn app() -> Html {
|
fn app() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Switch<Route> render={Switch::render(switch)} />
|
<Switch<Route> render={switch} />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,7 +206,7 @@ Here's how to implement a button that navigates to the `Home` route when clicked
|
|||||||
#[function_component(MyComponent)]
|
#[function_component(MyComponent)]
|
||||||
pub fn my_component() -> Html {
|
pub fn my_component() -> Html {
|
||||||
let navigator = use_navigator().unwrap();
|
let navigator = use_navigator().unwrap();
|
||||||
let onclick = Callback::from(move |_| navigator.push(Route::Home));
|
let onclick = Callback::from(move |_| navigator.push(&Route::Home));
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
@ -239,7 +239,7 @@ pub fn nav_items() -> Html {
|
|||||||
|
|
||||||
let go_home_button = {
|
let go_home_button = {
|
||||||
let navigator = navigator.clone();
|
let navigator = navigator.clone();
|
||||||
let onclick = Callback::from(move |_| navigator.push(Route::Home));
|
let onclick = Callback::from(move |_| navigator.push(&Route::Home));
|
||||||
html! {
|
html! {
|
||||||
<button {onclick}>{"click to go home"}</button>
|
<button {onclick}>{"click to go home"}</button>
|
||||||
}
|
}
|
||||||
@ -247,14 +247,14 @@ pub fn nav_items() -> Html {
|
|||||||
|
|
||||||
let go_to_first_post_button = {
|
let go_to_first_post_button = {
|
||||||
let navigator = navigator.clone();
|
let navigator = navigator.clone();
|
||||||
let onclick = Callback::from(move |_| navigator.push(Route::Post { id: "first-post".to_string() }));
|
let onclick = Callback::from(move |_| navigator.push(&Route::Post { id: "first-post".to_string() }));
|
||||||
html! {
|
html! {
|
||||||
<button {onclick}>{"click to go the first post"}</button>
|
<button {onclick}>{"click to go the first post"}</button>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let go_to_secure_button = {
|
let go_to_secure_button = {
|
||||||
let onclick = Callback::from(move |_| navigator.push(Route::Secure));
|
let onclick = Callback::from(move |_| navigator.push(&Route::Secure));
|
||||||
html! {
|
html! {
|
||||||
<button {onclick}>{"click to go to secure"}</button>
|
<button {onclick}>{"click to go to secure"}</button>
|
||||||
}
|
}
|
||||||
@ -278,7 +278,7 @@ identical with the function component case. Here's an example of a view function
|
|||||||
```rust ,ignore
|
```rust ,ignore
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
let navigator = ctx.link().navigator().unwrap();
|
let navigator = ctx.link().navigator().unwrap();
|
||||||
let onclick = Callback::from(move |_| navigator.push(MainRoute::Home));
|
let onclick = Callback::from(move |_| navigator.push(&MainRoute::Home));
|
||||||
html!{
|
html!{
|
||||||
<button {onclick}>{"Go Home"}</button>
|
<button {onclick}>{"Go Home"}</button>
|
||||||
}
|
}
|
||||||
@ -455,17 +455,17 @@ enum SettingsRoute {
|
|||||||
NotFound,
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_main(route: &MainRoute) -> Html {
|
fn switch_main(route: MainRoute) -> Html {
|
||||||
match route {
|
match route {
|
||||||
MainRoute::Home => html! {<h1>{"Home"}</h1>},
|
MainRoute::Home => html! {<h1>{"Home"}</h1>},
|
||||||
MainRoute::News => html! {<h1>{"News"}</h1>},
|
MainRoute::News => html! {<h1>{"News"}</h1>},
|
||||||
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
|
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
|
||||||
MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={Switch::render(switch_settings)} /> },
|
MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={switch_settings} /> },
|
||||||
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
|
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_settings(route: &SettingsRoute) -> Html {
|
fn switch_settings(route: SettingsRoute) -> Html {
|
||||||
match route {
|
match route {
|
||||||
SettingsRoute::Profile => html! {<h1>{"Profile"}</h1>},
|
SettingsRoute::Profile => html! {<h1>{"Profile"}</h1>},
|
||||||
SettingsRoute::Friends => html! {<h1>{"Friends"}</h1>},
|
SettingsRoute::Friends => html! {<h1>{"Friends"}</h1>},
|
||||||
@ -478,7 +478,7 @@ fn switch_settings(route: &SettingsRoute) -> Html {
|
|||||||
pub fn app() -> Html {
|
pub fn app() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Switch<MainRoute> render={Switch::render(switch_main)} />
|
<Switch<MainRoute> render={switch_main} />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: 'From 0.16.0 to 0.17.0'
|
||||||
|
---
|
||||||
|
|
||||||
|
## `Switch::render` is no longer needed
|
||||||
|
|
||||||
|
The `<Switch />` component now accepts a closure of `Fn(Routable) -> Html` as
|
||||||
|
the render function directly.
|
||||||
|
|
||||||
|
## `navigator` API
|
||||||
|
|
||||||
|
The History API has been replaced with the Navigator API.
|
||||||
Loading…
x
Reference in New Issue
Block a user