Skip to main content

wal_core/router/
mod.rs

1pub mod builder;
2pub(crate) mod not_found_component;
3
4use std::{cell::RefCell, collections::HashMap, rc::Rc};
5
6use crate::{component::node::AnyComponentNode, virtual_dom::dom};
7use gloo::utils::{body, history, window};
8use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
9use web_sys::{Element, Event, EventTarget};
10
11use self::not_found_component::NotFoundComponent;
12
13pub(crate) struct PageRenderer {
14    generator: Box<dyn Fn() -> Rc<RefCell<AnyComponentNode>>>,
15}
16
17impl PageRenderer {
18    pub fn new(generator: impl Fn() -> Rc<RefCell<AnyComponentNode>> + 'static) -> PageRenderer {
19        PageRenderer {
20            generator: Box::new(generator),
21        }
22    }
23
24    pub fn render(&self) -> Rc<RefCell<AnyComponentNode>> {
25        (*self.generator)()
26    }
27}
28
29thread_local! {
30    /// Static [Router] instance.
31    pub(crate) static ROUTER: RefCell<Router> = RefCell::new(Router::empty());
32}
33
34struct CurrentPage {
35    pub path: String,
36    pub page: Rc<RefCell<AnyComponentNode>>,
37}
38
39/// Router of the application. Handles routing in the application and correctly resolves paths.
40pub struct Router {
41    pages: HashMap<&'static str, PageRenderer>,
42    not_found_page: PageRenderer,
43    current: Option<CurrentPage>,
44}
45
46impl Router {
47    pub(crate) fn empty() -> Router {
48        Router {
49            pages: [].into(),
50            not_found_page: PageRenderer::new(|| {
51                AnyComponentNode::new_root_routing(NotFoundComponent, dom::get_root_element())
52            }),
53            current: None,
54        }
55    }
56
57    pub(crate) fn new(
58        pages: HashMap<&'static str, PageRenderer>,
59        not_found_page: PageRenderer,
60    ) -> Router {
61        Router {
62            pages,
63            not_found_page,
64            current: None,
65        }
66    }
67
68    /// Start of the application. Moves router instance. Should be called only *once* in application.
69    ///
70    /// # Example
71    /// ```
72    /// #[derive(Default)]
73    /// struct MainPage;
74    /// impl Component for MainPage {...}
75    ///
76    /// //..
77    ///
78    /// // Start of the application
79    /// RouterBuilder::default()
80    ///     .add_page::<MainPage>("/")
81    ///     .build()
82    ///     .start();
83    /// ```
84    pub fn start(self) {
85        std::panic::set_hook(Box::new(console_error_panic_hook::hook));
86
87        let click = Closure::<dyn Fn(Event)>::new(Self::click);
88        Self::add_event_listener(body().into(), "click", &click);
89        click.forget();
90
91        let route = Closure::<dyn Fn()>::new(Self::route);
92        Self::add_event_listener(window().into(), "popstate", &route);
93        route.forget();
94
95        ROUTER.with(move |router| {
96            let mut router = router.borrow_mut();
97            *router = self;
98        });
99
100        Self::route();
101    }
102
103    fn route() {
104        ROUTER.with(|router| {
105            let mut router = router.borrow_mut();
106            let pathname = window().location().pathname().unwrap();
107
108            if let Some(old_current) = &router.current {
109                if pathname.eq(&old_current.path) {
110                    return;
111                }
112            }
113
114            let old_current = router.current.take();
115            let page_renderer = router
116                .pages
117                .get(pathname.as_str())
118                .unwrap_or(&router.not_found_page);
119            let new_page = page_renderer.render();
120            let old_page = old_current.map(|x| x.page);
121
122            new_page.borrow_mut().view();
123            new_page
124                .borrow_mut()
125                .patch(old_page, &dom::get_root_element());
126
127            router.current = Some(CurrentPage {
128                path: pathname,
129                page: new_page,
130            });
131        });
132    }
133
134    const WAL_ROUTING_ATTR: &'static str = "data_link";
135
136    fn click(e: Event) {
137        let target = e.target().unwrap().unchecked_into::<Element>();
138        let matches = target
139            .closest(&format!("[{}]", Self::WAL_ROUTING_ATTR))
140            .unwrap();
141        if let Some(el) = matches {
142            e.prevent_default();
143            Self::navigate_to(el.get_attribute("href").unwrap().as_str());
144        }
145    }
146
147    fn navigate_to(url: &str) {
148        history()
149            .push_state_with_url(&JsValue::null(), "", Some(url))
150            .unwrap();
151        Self::route();
152    }
153
154    fn add_event_listener<T: ?Sized>(target: EventTarget, type_: &str, listener: &Closure<T>) {
155        target
156            .add_event_listener_with_callback(type_, listener.as_ref().unchecked_ref())
157            .unwrap();
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use crate::{
164        component::{behavior::Behavior, Component},
165        virtual_dom::{VNode, VText},
166    };
167    use wasm_bindgen_test::wasm_bindgen_test;
168
169    use super::{builder::RouterBuilder, Router, ROUTER};
170    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
171
172    const VALID_TEXT: &str = "";
173
174    #[wasm_bindgen_test]
175    fn empty() {
176        let empty = Router::empty();
177
178        assert_eq!(empty.pages.len(), 0);
179        assert!(empty.current.is_none());
180    }
181
182    struct Root;
183    impl Default for Root {
184        fn default() -> Self {
185            Self::new(())
186        }
187    }
188    impl Component for Root {
189        type Message = ();
190        type Properties = ();
191        fn new(_props: Self::Properties) -> Self {
192            Root
193        }
194        fn view(&self, _behavior: &mut impl Behavior<Self>) -> VNode {
195            VText::new(VALID_TEXT).into()
196        }
197        fn update(&mut self, _message: Self::Message) -> bool {
198            false
199        }
200    }
201
202    #[wasm_bindgen_test]
203    fn new_router() {
204        let router = RouterBuilder::default().add_page::<Root>("/").build();
205
206        assert!(router.pages.contains_key("/"));
207        assert_eq!(router.pages.len(), 1);
208        assert!(router.current.is_none());
209    }
210
211    #[wasm_bindgen_test]
212    fn start() {
213        let router = RouterBuilder::default().add_page::<Root>("/").build();
214        let router2 = RouterBuilder::default().add_page::<Root>("/").build();
215
216        router.start();
217
218        ROUTER.with(move |router| {
219            let router = router.borrow();
220            assert!(router.pages.keys().all(|x| router2.pages.contains_key(*x)));
221            assert!(router.current.is_some());
222            if let Some(cur) = &router.current {
223                assert_eq!(cur.path, "/");
224            }
225        });
226    }
227
228    struct Root2;
229    impl Component for Root2 {
230        type Message = ();
231        type Properties = ();
232        fn new(_props: Self::Properties) -> Self {
233            Root2
234        }
235        fn view(&self, _behavior: &mut impl Behavior<Self>) -> VNode {
236            VText::new(VALID_TEXT).into()
237        }
238        fn update(&mut self, _message: Self::Message) -> bool {
239            false
240        }
241    }
242
243    #[wasm_bindgen_test]
244    fn navigate_to() {
245        let router = RouterBuilder::default()
246            .add_page::<Root>("/")
247            .add_page::<Root>("/2")
248            .build();
249
250        router.start();
251
252        let url = "url";
253        Router::navigate_to(url);
254        ROUTER.with(move |router| {
255            let router = router.borrow();
256            assert!(router.current.is_some());
257            if let Some(cur) = &router.current {
258                assert_eq!(cur.path, format!("/{url}"));
259            }
260        });
261        Router::navigate_to("/2");
262        ROUTER.with(move |router| {
263            let router = router.borrow();
264            assert!(router.current.is_some());
265            if let Some(cur) = &router.current {
266                assert_eq!(cur.path, "/2");
267            }
268        });
269        Router::navigate_to("/");
270        ROUTER.with(move |router| {
271            let router = router.borrow();
272            assert!(router.current.is_some());
273            if let Some(cur) = &router.current {
274                assert_eq!(cur.path, "/");
275            }
276        });
277    }
278}