yew_router_nested/
service.rs

1//! Service that interfaces with the browser to handle routing.
2
3use yew::callback::Callback;
4
5use crate::route::{Route, RouteState};
6use cfg_if::cfg_if;
7use cfg_match::cfg_match;
8use std::marker::PhantomData;
9
10cfg_if! {
11    if #[cfg(feature = "std_web")] {
12        use stdweb::{
13            js,
14            unstable::{TryFrom, TryInto},
15            web::{event::PopStateEvent, window, EventListenerHandle, History, IEventTarget, Location},
16            Value,
17        };
18    } else if #[cfg(feature = "web_sys")] {
19        use web_sys::{History, Location, PopStateEvent};
20        use gloo::events::EventListener;
21        use wasm_bindgen::{JsValue as Value, JsCast};
22    }
23}
24
25/// A service that facilitates manipulation of the browser's URL bar and responding to browser events
26/// when users press 'forward' or 'back'.
27///
28/// The `T` determines what route state can be stored in the route service.
29#[derive(Debug)]
30pub struct RouteService<STATE = ()> {
31    history: History,
32    location: Location,
33    #[cfg(feature = "std_web")]
34    event_listener: Option<EventListenerHandle>,
35    #[cfg(feature = "web_sys")]
36    event_listener: Option<EventListener>,
37    phantom_data: PhantomData<STATE>,
38}
39
40impl<STATE> Default for RouteService<STATE>
41where
42    STATE: RouteState,
43{
44    fn default() -> Self {
45        RouteService::<STATE>::new()
46    }
47}
48
49impl<T> RouteService<T> {
50    /// Creates the route service.
51    pub fn new() -> RouteService<T> {
52        let (history, location) = cfg_match! {
53            feature = "std_web" => ({
54                (
55                    window().history(),
56                    window().location().expect("browser does not support location API")
57                )
58            }),
59            feature = "web_sys" => ({
60                let window = web_sys::window().unwrap();
61                (
62                    window.history().expect("browser does not support history API"),
63                    window.location()
64                )
65            }),
66        };
67
68        RouteService {
69            history,
70            location,
71            event_listener: None,
72            phantom_data: PhantomData,
73        }
74    }
75
76    #[inline]
77    fn get_route_from_location(location: &Location) -> String {
78        let path = location.pathname().unwrap();
79        let query = location.search().unwrap();
80        let fragment = location.hash().unwrap();
81        format_route_string(&path, &query, &fragment)
82    }
83
84    /// Gets the path name of the current url.
85    pub fn get_path(&self) -> String {
86        self.location.pathname().unwrap()
87    }
88
89    /// Gets the query string of the current url.
90    pub fn get_query(&self) -> String {
91        self.location.search().unwrap()
92    }
93
94    /// Gets the fragment of the current url.
95    pub fn get_fragment(&self) -> String {
96        self.location.hash().unwrap()
97    }
98}
99
100impl<STATE> RouteService<STATE>
101where
102    STATE: RouteState,
103{
104    /// Registers a callback to the route service.
105    /// Callbacks will be called when the History API experiences a change such as
106    /// popping a state off of its stack when the forward or back buttons are pressed.
107    pub fn register_callback(&mut self, callback: Callback<Route<STATE>>) {
108        let cb = move |event: PopStateEvent| {
109            let state_value: Value = event.state();
110            let state_string: String = cfg_match! {
111                feature = "std_web" => String::try_from(state_value).unwrap_or_default(),
112                feature = "web_sys" => state_value.as_string().unwrap_or_default(),
113            };
114            let state: STATE = serde_json::from_str(&state_string).unwrap_or_else(|_| {
115                log::error!("Could not deserialize state string");
116                STATE::default()
117            });
118
119            // Can't use the existing location, because this is a callback, and can't move it in
120            // here.
121            let location: Location = cfg_match! {
122                feature = "std_web" => window().location().unwrap(),
123                feature = "web_sys" => web_sys::window().unwrap().location(),
124            };
125            let route: String = Self::get_route_from_location(&location);
126
127            callback.emit(Route { route, state })
128        };
129
130        cfg_if! {
131            if #[cfg(feature = "std_web")] {
132                self.event_listener = Some(window().add_event_listener(move |event: PopStateEvent| {
133                    cb(event)
134                }));
135            } else if #[cfg(feature = "web_sys")] {
136                self.event_listener = Some(EventListener::new(web_sys::window().unwrap().as_ref(), "popstate", move |event| {
137                    let event: PopStateEvent = event.clone().dyn_into().unwrap();
138                    cb(event)
139                }));
140            }
141        };
142    }
143
144    /// Sets the browser's url bar to contain the provided route,
145    /// and creates a history entry that can be navigated via the forward and back buttons.
146    ///
147    /// The route should be a relative path that starts with a `/`.
148    pub fn set_route(&mut self, route: &str, state: STATE) {
149        let state_string: String = serde_json::to_string(&state).unwrap_or_else(|_| {
150            log::error!("Could not serialize state string");
151            "".to_string()
152        });
153        cfg_match! {
154            feature = "std_web" => ({
155                self.history.push_state(state_string, "", Some(route));
156            }),
157            feature = "web_sys" => ({
158                let _ = self.history.push_state_with_url(&Value::from_str(&state_string), "", Some(route));
159            }),
160        };
161    }
162
163    /// Replaces the route with another one removing the most recent history event and
164    /// creating another history event in its place.
165    pub fn replace_route(&mut self, route: &str, state: STATE) {
166        let state_string: String = serde_json::to_string(&state).unwrap_or_else(|_| {
167            log::error!("Could not serialize state string");
168            "".to_string()
169        });
170        cfg_match! {
171            feature = "std_web" => ({
172                let _ = self.history.replace_state(state_string, "", Some(route));
173            }),
174            feature = "web_sys" => ({
175                let _ = self.history.replace_state_with_url(&Value::from_str(&state_string), "", Some(route));
176            }),
177        };
178    }
179
180    /// Gets the concatenated path, query, and fragment.
181    pub fn get_route(&self) -> Route<STATE> {
182        let route_string = Self::get_route_from_location(&self.location);
183        let state: STATE = get_state_string(&self.history)
184            .or_else(|| {
185                log::trace!("History state is empty");
186                None
187            })
188            .and_then(|state_string| -> Option<STATE> {
189                serde_json::from_str(&state_string)
190                    .ok()
191                    .or_else(|| {
192                        log::error!("Could not deserialize state string");
193                        None
194                    })
195                    .and_then(std::convert::identity) // flatten
196            })
197            .unwrap_or_default();
198        Route {
199            route: route_string,
200            state,
201        }
202    }
203}
204
205/// Formats a path, query, and fragment into a string.
206///
207/// # Note
208/// This expects that all three already have their expected separators (?, #, etc)
209pub(crate) fn format_route_string(path: &str, query: &str, fragment: &str) -> String {
210    format!(
211        "{path}{query}{fragment}",
212        path = path,
213        query = query,
214        fragment = fragment
215    )
216}
217
218fn get_state(history: &History) -> Value {
219    cfg_match! {
220        feature = "std_web" => js!(
221            return @{history}.state;
222        ),
223        feature = "web_sys" => history.state().unwrap(),
224    }
225}
226
227fn get_state_string(history: &History) -> Option<String> {
228    cfg_match! {
229        feature = "std_web" => get_state(history).try_into().ok(),
230        feature = "web_sys" => get_state(history).as_string(),
231    }
232}