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 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
39pub 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 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}