gloo_history/
browser.rs

1use std::{any::Any, borrow::Cow, cell::RefCell, fmt, rc::Rc};
2
3use gloo_events::EventListener;
4use gloo_utils::window;
5use wasm_bindgen::{JsValue, UnwrapThrowExt};
6use web_sys::Url;
7
8use crate::history::History;
9use crate::listener::HistoryListener;
10use crate::location::Location;
11use crate::state::{HistoryState, StateMap};
12use crate::utils::WeakCallback;
13#[cfg(feature = "query")]
14use crate::{error::HistoryResult, query::ToQuery};
15
16/// A [`History`] that is implemented with [`web_sys::History`] that provides native browser
17/// history and state access.
18#[derive(Clone)]
19pub struct BrowserHistory {
20    inner: web_sys::History,
21    states: Rc<RefCell<StateMap>>,
22    callbacks: Rc<RefCell<Vec<WeakCallback>>>,
23}
24
25impl fmt::Debug for BrowserHistory {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.debug_struct("BrowserHistory").finish()
28    }
29}
30
31impl PartialEq for BrowserHistory {
32    fn eq(&self, _rhs: &Self) -> bool {
33        // All browser histories are created equal.
34        true
35    }
36}
37
38impl History for BrowserHistory {
39    fn len(&self) -> usize {
40        self.inner.length().expect_throw("failed to get length.") as usize
41    }
42
43    fn go(&self, delta: isize) {
44        self.inner
45            .go_with_delta(delta as i32)
46            .expect_throw("failed to call go.")
47    }
48
49    fn push<'a>(&self, route: impl Into<Cow<'a, str>>) {
50        let url = route.into();
51        self.inner
52            .push_state_with_url(&Self::create_history_state().1, "", Some(&url))
53            .expect_throw("failed to push state.");
54
55        self.notify_callbacks();
56    }
57
58    fn replace<'a>(&self, route: impl Into<Cow<'a, str>>) {
59        let url = route.into();
60        self.inner
61            .replace_state_with_url(&Self::create_history_state().1, "", Some(&url))
62            .expect_throw("failed to replace history.");
63
64        self.notify_callbacks();
65    }
66
67    fn push_with_state<'a, T>(&self, route: impl Into<Cow<'a, str>>, state: T)
68    where
69        T: 'static,
70    {
71        let url = route.into();
72
73        let (id, history_state) = Self::create_history_state();
74
75        let mut states = self.states.borrow_mut();
76        states.insert(id, Rc::new(state) as Rc<dyn Any>);
77
78        self.inner
79            .push_state_with_url(&history_state, "", Some(&url))
80            .expect_throw("failed to push state.");
81
82        drop(states);
83        self.notify_callbacks();
84    }
85
86    fn replace_with_state<'a, T>(&self, route: impl Into<Cow<'a, str>>, state: T)
87    where
88        T: 'static,
89    {
90        let url = route.into();
91
92        let (id, history_state) = Self::create_history_state();
93
94        let mut states = self.states.borrow_mut();
95        states.insert(id, Rc::new(state) as Rc<dyn Any>);
96
97        self.inner
98            .replace_state_with_url(&history_state, "", Some(&url))
99            .expect_throw("failed to replace state.");
100
101        drop(states);
102        self.notify_callbacks();
103    }
104
105    #[cfg(feature = "query")]
106    fn push_with_query<'a, Q>(
107        &self,
108        route: impl Into<Cow<'a, str>>,
109        query: Q,
110    ) -> HistoryResult<(), Q::Error>
111    where
112        Q: ToQuery,
113    {
114        let route = route.into();
115        let query = query.to_query()?;
116
117        let url = Self::combine_url(&route, &query);
118
119        self.inner
120            .push_state_with_url(&Self::create_history_state().1, "", Some(&url))
121            .expect_throw("failed to push history.");
122
123        self.notify_callbacks();
124        Ok(())
125    }
126
127    #[cfg(feature = "query")]
128    fn replace_with_query<'a, Q>(
129        &self,
130        route: impl Into<Cow<'a, str>>,
131        query: Q,
132    ) -> HistoryResult<(), Q::Error>
133    where
134        Q: ToQuery,
135    {
136        let route = route.into();
137        let query = query.to_query()?;
138
139        let url = Self::combine_url(&route, &query);
140
141        self.inner
142            .replace_state_with_url(&Self::create_history_state().1, "", Some(&url))
143            .expect_throw("failed to replace history.");
144
145        self.notify_callbacks();
146        Ok(())
147    }
148
149    #[cfg(feature = "query")]
150    fn push_with_query_and_state<'a, Q, T>(
151        &self,
152        route: impl Into<Cow<'a, str>>,
153        query: Q,
154        state: T,
155    ) -> HistoryResult<(), Q::Error>
156    where
157        Q: ToQuery,
158        T: 'static,
159    {
160        let (id, history_state) = Self::create_history_state();
161
162        let mut states = self.states.borrow_mut();
163        states.insert(id, Rc::new(state) as Rc<dyn Any>);
164
165        let route = route.into();
166        let query = query.to_query()?;
167
168        let url = Self::combine_url(&route, &query);
169
170        self.inner
171            .push_state_with_url(&history_state, "", Some(&url))
172            .expect_throw("failed to push history.");
173
174        drop(states);
175        self.notify_callbacks();
176        Ok(())
177    }
178
179    #[cfg(feature = "query")]
180    fn replace_with_query_and_state<'a, Q, T>(
181        &self,
182        route: impl Into<Cow<'a, str>>,
183        query: Q,
184        state: T,
185    ) -> HistoryResult<(), Q::Error>
186    where
187        Q: ToQuery,
188        T: 'static,
189    {
190        let (id, history_state) = Self::create_history_state();
191
192        let mut states = self.states.borrow_mut();
193        states.insert(id, Rc::new(state) as Rc<dyn Any>);
194
195        let route = route.into();
196        let query = query.to_query()?;
197
198        let url = Self::combine_url(&route, &query);
199
200        self.inner
201            .replace_state_with_url(&history_state, "", Some(&url))
202            .expect_throw("failed to replace history.");
203
204        drop(states);
205        self.notify_callbacks();
206        Ok(())
207    }
208
209    fn listen<CB>(&self, callback: CB) -> HistoryListener
210    where
211        CB: Fn() + 'static,
212    {
213        // Callbacks do not receive a copy of [`History`] to prevent reference cycle.
214        let cb = Rc::new(callback) as Rc<dyn Fn()>;
215
216        self.callbacks.borrow_mut().push(Rc::downgrade(&cb));
217
218        HistoryListener { _listener: cb }
219    }
220
221    fn location(&self) -> Location {
222        let loc = window().location();
223
224        let history_state = self.inner.state().expect_throw("failed to get state");
225        let history_state = serde_wasm_bindgen::from_value::<HistoryState>(history_state).ok();
226
227        let id = history_state.map(|m| m.id());
228
229        let states = self.states.borrow();
230
231        Location {
232            path: loc.pathname().expect_throw("failed to get pathname").into(),
233            query_str: loc
234                .search()
235                .expect_throw("failed to get location query.")
236                .into(),
237            hash: loc
238                .hash()
239                .expect_throw("failed to get location hash.")
240                .into(),
241            state: id.and_then(|m| states.get(&m).cloned()),
242            id,
243        }
244    }
245}
246
247impl Default for BrowserHistory {
248    fn default() -> Self {
249        // We create browser history only once.
250        thread_local! {
251            static BROWSER_HISTORY: (BrowserHistory, EventListener) = {
252                let window = window();
253
254                let inner = window
255                    .history()
256                    .expect_throw("Failed to create browser history. Are you using a browser?");
257                let callbacks = Rc::default();
258
259                let history = BrowserHistory {
260                    inner,
261                    callbacks,
262                    states: Rc::default(),
263                };
264
265                let listener = {
266                    let history = history.clone();
267
268                    // Listens to popstate.
269                    EventListener::new(&window, "popstate", move |_| {
270                        history.notify_callbacks();
271                    })
272                };
273
274                (history, listener)
275            };
276        }
277
278        BROWSER_HISTORY.with(|(history, _)| history.clone())
279    }
280}
281
282impl BrowserHistory {
283    /// Creates a new [`BrowserHistory`]
284    pub fn new() -> Self {
285        Self::default()
286    }
287
288    fn notify_callbacks(&self) {
289        crate::utils::notify_callbacks(self.callbacks.clone());
290    }
291
292    fn create_history_state() -> (u32, JsValue) {
293        let history_state = HistoryState::new();
294
295        (
296            history_state.id(),
297            serde_wasm_bindgen::to_value(&history_state)
298                .expect_throw("fails to create history state."),
299        )
300    }
301
302    pub(crate) fn combine_url(route: &str, query: &str) -> String {
303        let href = window()
304            .location()
305            .href()
306            .expect_throw("Failed to read location href");
307
308        let url = Url::new_with_base(route, &href).expect_throw("current url is not valid.");
309
310        url.set_search(query);
311
312        url.href()
313    }
314}