plaster_router/
lib.rs

1use js_sys::Function;
2use plaster::callback::Callback;
3use route_recognizer::{Params, Router as RecRouter};
4use serde_derive::{Deserialize, Serialize};
5use std::sync::{Arc, Mutex};
6use wasm_bindgen::prelude::*;
7use wasm_bindgen::{JsCast, JsValue};
8use web_sys::{window, CustomEvent, CustomEventInit};
9
10use log::trace;
11pub use plaster_router_macro::Routes;
12
13pub struct Router<T> {
14    routes: Vec<fn(Params) -> T>,
15    index_router: RecRouter<usize>,
16    current_path: Arc<Mutex<String>>,
17    listener: Closure<dyn FnMut(CustomEvent)>,
18    callback: Callback<()>,
19}
20
21impl<T> Router<T> {
22    pub fn new(callback: Callback<()>) -> Router<T> {
23        let win = window().expect("need a window context");
24        let path = if cfg!(not(feature = "mobile")) {
25            win.location().pathname().unwrap_or("/".to_string())
26        } else {
27            "/".to_string()
28        };
29        trace!("initial route: {}", &path);
30        let current_path = Arc::new(Mutex::new(path));
31        let current_path_c = current_path.clone();
32        let callback_c = callback.clone();
33
34        let listener_callback = Closure::wrap(Box::new(move |e: CustomEvent| {
35            let ev: RouteEvent = e
36                .detail()
37                .into_serde()
38                .expect("could not deserialize route event");
39            trace!("route change: {}", &ev.route);
40            *current_path_c.lock().unwrap() = ev.route;
41            callback_c.emit(());
42        }) as Box<dyn FnMut(_)>);
43
44        let listener_function: &Function = listener_callback.as_ref().unchecked_ref();
45
46        win.add_event_listener_with_callback("plasterroutechange", listener_function)
47            .expect("could not attach global event listener");
48
49        if cfg!(not(feature = "mobile")) {
50            win.add_event_listener_with_callback("popstate", listener_function)
51                .expect("could not attach popstate event listener");
52        }
53
54        Router {
55            routes: Vec::new(),
56            index_router: RecRouter::new(),
57            current_path: current_path,
58            listener: listener_callback,
59            callback: callback,
60        }
61    }
62
63    pub fn add_route(&mut self, route: &str, closure: fn(Params) -> T) {
64        trace!("added route: {}", route);
65        let index = self.routes.len();
66        self.routes.push(closure);
67        self.index_router.add(route, index);
68    }
69
70    pub fn navigate(&mut self, path: &str) {
71        *self.current_path.lock().unwrap() = path.to_string();
72        if cfg!(not(feature = "mobile")) {
73            self.push_state();
74        }
75        self.callback.emit(());
76    }
77
78    pub fn resolve(&self) -> Option<T> {
79        let route_match = self
80            .index_router
81            .recognize(&self.current_path.lock().unwrap())
82            .ok();
83        route_match.map(|m| self.routes.get(m.handler.clone()).unwrap()(m.params))
84    }
85
86    pub fn current_route(&self) -> String {
87        self.current_path.lock().unwrap().clone()
88    }
89
90    pub fn set_route(&self, path: &str) {
91        *self.current_path.lock().unwrap() = path.to_string();
92    }
93
94    fn push_state(&self) {
95        match window().expect("need a window context").history() {
96            Ok(history) => {
97                history
98                    .push_state_with_url(
99                        &JsValue::NULL,
100                        "",
101                        Some(&self.current_path.lock().unwrap()),
102                    )
103                    .expect("could not pushState");
104            }
105            Err(_) => (),
106        }
107    }
108}
109
110impl<T> Drop for Router<T> {
111    fn drop(&mut self) {
112        window()
113            .expect("need window context")
114            .remove_event_listener_with_callback(
115                "plasterroutechange",
116                self.listener.as_ref().unchecked_ref(),
117            )
118            .expect("could not remove event listener");
119    }
120}
121
122pub trait Routes<T> {
123    fn router(callback: Callback<()>) -> Router<T>;
124}
125
126pub fn route_to(path: &str) {
127    let win = window().expect("need window context");
128
129    if cfg!(not(feature = "mobile")) {
130        win.history()
131            .expect("history API unavailable")
132            .push_state_with_url(&JsValue::NULL, "", Some(path))
133            .expect("could not pushState");
134    }
135
136    let mut init = CustomEventInit::new();
137    init.detail(
138        &JsValue::from_serde(&RouteEvent {
139            route: path.to_owned(),
140        })
141        .unwrap(),
142    );
143    let event = CustomEvent::new_with_event_init_dict("plasterroutechange", &init)
144        .expect("could not create CustomEvent");
145    win.dispatch_event(&event)
146        .expect("could not dispatch route change");
147}
148
149#[derive(Serialize, Deserialize)]
150struct RouteEvent {
151    route: String,
152}