Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_router/
navigator.rs

1use std::borrow::Cow;
2
3use crate::history::{AnyHistory, History, HistoryError, HistoryResult};
4use crate::query::ToQuery;
5use crate::routable::Routable;
6
7pub type NavigationError = HistoryError;
8pub type NavigationResult<T> = HistoryResult<T>;
9
10/// The kind of Navigator Provider.
11#[derive(Debug, PartialEq, Eq, Clone, Copy)]
12pub enum NavigatorKind {
13    /// Browser History.
14    Browser,
15    /// Hash History.
16    Hash,
17    /// Memory History.
18    Memory,
19}
20
21/// A struct to navigate between locations.
22#[derive(Debug, PartialEq, Clone)]
23pub struct Navigator {
24    inner: AnyHistory,
25    basename: Option<String>,
26}
27
28impl Navigator {
29    pub(crate) fn new(history: AnyHistory, basename: Option<String>) -> Self {
30        Self {
31            inner: history,
32            basename,
33        }
34    }
35
36    /// Returns basename of current navigator.
37    pub fn basename(&self) -> Option<&str> {
38        self.basename.as_deref()
39    }
40
41    /// Navigate back 1 page.
42    pub fn back(&self) {
43        self.go(-1);
44    }
45
46    /// Navigate forward 1 page.
47    pub fn forward(&self) {
48        self.go(1);
49    }
50
51    /// Navigate to a specific page with a `delta` relative to current page.
52    ///
53    /// See: <https://developer.mozilla.org/en-US/docs/Web/API/History/go>
54    pub fn go(&self, delta: isize) {
55        self.inner.go(delta);
56    }
57
58    /// Pushes a [`Routable`] entry.
59    pub fn push<R>(&self, route: &R)
60    where
61        R: Routable,
62    {
63        self.inner.push(self.prefix_basename(&route.to_path()));
64    }
65
66    /// Replaces the current history entry with provided [`Routable`] and [`None`] state.
67    pub fn replace<R>(&self, route: &R)
68    where
69        R: Routable,
70    {
71        self.inner.replace(self.prefix_basename(&route.to_path()));
72    }
73
74    /// Pushes a [`Routable`] entry with state.
75    pub fn push_with_state<R, T>(&self, route: &R, state: T)
76    where
77        R: Routable,
78        T: 'static,
79    {
80        self.inner
81            .push_with_state(self.prefix_basename(&route.to_path()), state);
82    }
83
84    /// Replaces the current history entry with provided [`Routable`] and state.
85    pub fn replace_with_state<R, T>(&self, route: &R, state: T)
86    where
87        R: Routable,
88        T: 'static,
89    {
90        self.inner
91            .replace_with_state(self.prefix_basename(&route.to_path()), state);
92    }
93
94    /// Same as `.push()` but affix the queries to the end of the route.
95    pub fn push_with_query<R, Q>(&self, route: &R, query: Q) -> Result<(), Q::Error>
96    where
97        R: Routable,
98        Q: ToQuery,
99    {
100        self.inner
101            .push_with_query(self.prefix_basename(&route.to_path()), query)
102    }
103
104    /// Same as `.replace()` but affix the queries to the end of the route.
105    pub fn replace_with_query<R, Q>(&self, route: &R, query: Q) -> Result<(), Q::Error>
106    where
107        R: Routable,
108        Q: ToQuery,
109    {
110        self.inner
111            .replace_with_query(self.prefix_basename(&route.to_path()), query)
112    }
113
114    /// Same as `.push_with_state()` but affix the queries to the end of the route.
115    pub fn push_with_query_and_state<R, Q, T>(
116        &self,
117        route: &R,
118        query: Q,
119        state: T,
120    ) -> Result<(), Q::Error>
121    where
122        R: Routable,
123        Q: ToQuery,
124        T: 'static,
125    {
126        self.inner
127            .push_with_query_and_state(self.prefix_basename(&route.to_path()), query, state)
128    }
129
130    /// Same as `.replace_with_state()` but affix the queries to the end of the route.
131    pub fn replace_with_query_and_state<R, Q, T>(
132        &self,
133        route: &R,
134        query: Q,
135        state: T,
136    ) -> Result<(), Q::Error>
137    where
138        R: Routable,
139        Q: ToQuery,
140        T: 'static,
141    {
142        self.inner.replace_with_query_and_state(
143            self.prefix_basename(&route.to_path()),
144            query,
145            state,
146        )
147    }
148
149    /// Returns the Navigator kind.
150    pub fn kind(&self) -> NavigatorKind {
151        match &self.inner {
152            AnyHistory::Browser(_) => NavigatorKind::Browser,
153            AnyHistory::Hash(_) => NavigatorKind::Hash,
154            AnyHistory::Memory(_) => NavigatorKind::Memory,
155        }
156    }
157
158    pub(crate) fn prefix_basename<'a>(&self, route_s: &'a str) -> Cow<'a, str> {
159        match self.basename() {
160            Some(base) => {
161                if base.is_empty() && route_s.is_empty() {
162                    Cow::from("/")
163                } else {
164                    Cow::from(format!("{base}{route_s}"))
165                }
166            }
167            None => route_s.into(),
168        }
169    }
170
171    pub(crate) fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> {
172        match self.basename() {
173            Some(m) => {
174                let mut path = strip_basename_prefix(&path, m)
175                    .map(|rest| Cow::Owned(rest.to_owned()))
176                    .unwrap_or(path);
177
178                if !path.starts_with('/') {
179                    path = format!("/{path}").into();
180                }
181
182                path
183            }
184            None => path,
185        }
186    }
187}
188
189/// Strips `basename` from the start of `path`, enforcing a segment boundary.
190///
191/// Returns the remainder after stripping, or `None` when the prefix either
192/// doesn't match or matches only a partial path segment (e.g. basename `/base`
193/// must not strip `/baseball`).
194fn strip_basename_prefix<'a>(path: &'a str, basename: &str) -> Option<&'a str> {
195    path.strip_prefix(basename)
196        .filter(|rest| rest.is_empty() || rest.starts_with('/'))
197}
198
199#[cfg(test)]
200mod tests {
201    use std::borrow::Cow;
202
203    use super::Navigator;
204    use crate::history::{AnyHistory, MemoryHistory};
205
206    fn make_nav(basename: Option<&str>) -> Navigator {
207        Navigator::new(
208            AnyHistory::from(MemoryHistory::default()),
209            basename.map(str::to_owned),
210        )
211    }
212
213    #[test]
214    fn strip_exact_match() {
215        let nav = make_nav(Some("/base"));
216        assert_eq!(nav.strip_basename(Cow::from("/base")), "/");
217    }
218
219    #[test]
220    fn strip_with_trailing_path() {
221        let nav = make_nav(Some("/base"));
222        assert_eq!(nav.strip_basename(Cow::from("/base/page")), "/page");
223        assert_eq!(nav.strip_basename(Cow::from("/base/a/b/c")), "/a/b/c");
224    }
225
226    #[test]
227    fn strip_partial_segment_rejected() {
228        let nav = make_nav(Some("/base"));
229        // `/base` must NOT be stripped from paths that merely start with those bytes
230        assert_eq!(nav.strip_basename(Cow::from("/baseball")), "/baseball");
231        assert_eq!(nav.strip_basename(Cow::from("/base-2")), "/base-2");
232        assert_eq!(nav.strip_basename(Cow::from("/basefoo")), "/basefoo");
233    }
234
235    #[test]
236    fn strip_no_match() {
237        let nav = make_nav(Some("/base"));
238        assert_eq!(nav.strip_basename(Cow::from("/other/page")), "/other/page");
239    }
240
241    #[test]
242    fn strip_no_basename() {
243        let nav = make_nav(None);
244        assert_eq!(nav.strip_basename(Cow::from("/any/path")), "/any/path");
245    }
246}