yew_router/
router.rs

1//! Router Component.
2use std::rc::Rc;
3
4use yew::prelude::*;
5use yew::virtual_dom::AttrValue;
6
7use crate::history::{AnyHistory, BrowserHistory, HashHistory, History, Location};
8use crate::navigator::Navigator;
9use crate::utils::{base_url, strip_slash_suffix};
10
11/// Props for [`Router`].
12#[derive(Properties, PartialEq, Clone)]
13pub struct RouterProps {
14    #[prop_or_default]
15    pub children: Html,
16    pub history: AnyHistory,
17    #[prop_or_default]
18    pub basename: Option<AttrValue>,
19}
20
21#[derive(Clone)]
22pub(crate) struct LocationContext {
23    location: Location,
24    // Counter to force update.
25    ctr: u32,
26}
27
28impl LocationContext {
29    pub fn location(&self) -> Location {
30        self.location.clone()
31    }
32}
33
34impl PartialEq for LocationContext {
35    fn eq(&self, rhs: &Self) -> bool {
36        self.ctr == rhs.ctr
37    }
38}
39
40impl Reducible for LocationContext {
41    type Action = Location;
42
43    fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
44        Self {
45            location: action,
46            ctr: self.ctr + 1,
47        }
48        .into()
49    }
50}
51
52#[derive(Clone, PartialEq)]
53pub(crate) struct NavigatorContext {
54    navigator: Navigator,
55}
56
57impl NavigatorContext {
58    pub fn navigator(&self) -> Navigator {
59        self.navigator.clone()
60    }
61}
62
63/// The base router.
64///
65/// The implementation is separated to make sure <Router /> has the same virtual dom layout as
66/// the <BrowserRouter /> and <HashRouter />.
67#[function_component(BaseRouter)]
68fn base_router(props: &RouterProps) -> Html {
69    let RouterProps {
70        history,
71        children,
72        basename,
73    } = props.clone();
74
75    let loc_ctx = use_reducer(|| LocationContext {
76        location: history.location(),
77        ctr: 0,
78    });
79
80    let basename = basename.map(|m| strip_slash_suffix(&m).to_string());
81    let navi_ctx = NavigatorContext {
82        navigator: Navigator::new(history.clone(), basename),
83    };
84
85    {
86        let loc_ctx_dispatcher = loc_ctx.dispatcher();
87
88        use_effect_with(history, move |history| {
89            let history = history.clone();
90            // Force location update when history changes.
91            loc_ctx_dispatcher.dispatch(history.location());
92
93            let history_cb = {
94                let history = history.clone();
95                move || loc_ctx_dispatcher.dispatch(history.location())
96            };
97
98            let listener = history.listen(history_cb);
99
100            // We hold the listener in the destructor.
101            move || {
102                std::mem::drop(listener);
103            }
104        });
105    }
106
107    html! {
108        <ContextProvider<NavigatorContext> context={navi_ctx}>
109            <ContextProvider<LocationContext> context={(*loc_ctx).clone()}>
110                {children}
111            </ContextProvider<LocationContext>>
112        </ContextProvider<NavigatorContext>>
113    }
114}
115
116/// The Router component.
117///
118/// This provides location and navigator context to its children and switches.
119///
120/// If you are building a web application, you may want to consider using [`BrowserRouter`] instead.
121///
122/// You only need one `<Router />` for each application.
123#[function_component(Router)]
124pub fn router(props: &RouterProps) -> Html {
125    html! {
126        <BaseRouter ..{props.clone()} />
127    }
128}
129
130/// Props for [`BrowserRouter`] and [`HashRouter`].
131#[derive(Properties, PartialEq, Clone)]
132pub struct ConcreteRouterProps {
133    pub children: Html,
134    #[prop_or_default]
135    pub basename: Option<AttrValue>,
136}
137
138/// A [`Router`] that provides location information and navigator via [`BrowserHistory`].
139///
140/// This Router uses browser's native history to manipulate session history
141/// and uses regular URL as route.
142///
143/// # Note
144///
145/// The router will by default use the value declared in `<base href="..." />` as its basename.
146/// You may also specify a different basename with props.
147#[function_component(BrowserRouter)]
148pub fn browser_router(props: &ConcreteRouterProps) -> Html {
149    let ConcreteRouterProps { children, basename } = props.clone();
150    let history = use_state(|| AnyHistory::from(BrowserHistory::new()));
151
152    // We acknowledge based in `<base href="..." />`
153    let basename = basename.map(|m| m.to_string()).or_else(base_url);
154
155    html! {
156        <BaseRouter history={(*history).clone()} {basename}>
157            {children}
158        </BaseRouter>
159    }
160}
161
162/// A [`Router`] that provides location information and navigator via [`HashHistory`].
163///
164/// This Router uses browser's native history to manipulate session history
165/// and stores route in hash fragment.
166///
167/// # Warning
168///
169/// Prefer [`BrowserRouter`] whenever possible and use this as a last resort.
170#[function_component(HashRouter)]
171pub fn hash_router(props: &ConcreteRouterProps) -> Html {
172    let ConcreteRouterProps { children, basename } = props.clone();
173    let history = use_state(|| AnyHistory::from(HashHistory::new()));
174
175    html! {
176        <BaseRouter history={(*history).clone()} {basename}>
177            {children}
178        </BaseRouter>
179    }
180}