use hirola_core::prelude::signal::{Mutable, MutableSignalCloned, SignalExt};
use hirola_core::prelude::*;
use std::collections::HashMap;
use std::fmt;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
use web_sys::{Element, Event};
use crate::Dom;
use super::App;
#[derive(Clone)]
pub struct Router<S: 'static = ()> {
current: Mutable<String>,
pub(crate) handler: matchit::Router<fn(&App<S>) -> Dom>,
pub(crate) not_found: Box<fn(&App<S>) -> Dom>,
}
impl<S> fmt::Debug for Router<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Router")
.field("current", &self.current)
.field(
"handler",
&format_args!("matchit::Router<fn(&App<S>) -> Dom>"),
)
.finish()
}
}
impl<S: Clone + 'static> Router<S> {
pub fn new() -> Self {
let mut path = String::from("/");
if let Some(window) = web_sys::window() {
path = window.location().pathname().unwrap_or("/".to_string());
}
Router {
current: Mutable::new(path),
handler: Default::default(),
not_found: Box::new(|_| Dom::text_node("Not Found")),
}
}
pub fn current_params(&self) -> HashMap<String, String> {
let path = self.current.get_cloned();
let binding = &self.handler;
match binding.at(&path) {
Ok(inner) => {
let params = &inner.params.clone();
let params = params.iter().fold(HashMap::new(), |mut map, c| {
map.insert(c.0.to_string(), c.1.to_string());
map
});
params
}
Err(_) => HashMap::new(),
}
}
pub fn push(&self, path: &str) {
let window = web_sys::window().unwrap();
window
.history()
.unwrap()
.push_state_with_url(&JsValue::default(), "", Some(path))
.unwrap();
self.current.set(path.to_owned());
}
pub fn link(&self) -> Box<dyn FnOnce(&Dom) + 'static> {
let router = self.clone();
let cb = move |node: &Dom| {
let router = router.clone();
let handle_click = move |e: Event| {
e.prevent_default();
let element = e.current_target().unwrap().dyn_into::<Element>().unwrap();
let href = element.get_attribute("href").unwrap();
router.push(&href);
};
node.event("click", handle_click);
};
Box::new(cb)
}
pub fn signal(&self) -> MutableSignalCloned<String> {
self.current.signal_cloned()
}
pub fn render(&self, app: &App<S>, parent: &Dom) -> Dom {
let router = &self.handler;
let current = self.current.clone();
let handle_hash = Closure::wrap(Box::new(move |_evt: web_sys::Event| {
let l: String = web_sys::window()
.unwrap()
.location()
.hash()
.unwrap()
.chars()
.skip(1)
.collect();
log::debug!("hash handle : {l}");
let h = web_sys::window().unwrap().history().unwrap();
h.replace_state_with_url(&JsValue::NULL, "", Some(l.as_str()))
.unwrap();
current.set(l.to_string());
}) as Box<dyn Fn(_)>);
web_sys::window()
.unwrap()
.set_onhashchange(Some(handle_hash.as_ref().unchecked_ref()));
handle_hash.forget();
let current = self.current.clone();
let handle_pop = Closure::wrap(Box::new(move |_evt: web_sys::Event| {
let path_name = web_sys::window().unwrap().location().pathname().unwrap();
if web_sys::window()
.unwrap()
.location()
.hash()
.unwrap()
.chars()
.count()
> 0
{
log::debug!("hash detected");
return;
}
current.set(path_name.to_string());
log::debug!("pop handle : {path_name}");
}) as Box<dyn Fn(_)>);
web_sys::window()
.unwrap()
.set_onpopstate(Some(handle_pop.as_ref().unchecked_ref()));
handle_pop.forget();
let route = &self.current.clone();
let router = router.clone();
let app = app.clone();
let node = parent.clone();
let not_found = self.not_found.clone();
let wait_for_next_route = route
.signal_cloned()
.map(move |route_match| {
let match_result = router.at(&route_match);
let page_fn = match match_result {
Ok(v) => v.value,
Err(_) => ¬_found,
};
let builder = page_fn(&app);
let dom = Dom::fragment();
dom.append_child(&builder);
node.replace_children_with(&dom);
let window = web_sys::window().unwrap();
window
.history()
.unwrap()
.push_state_with_url(&JsValue::default(), "", Some(&route_match))
.unwrap();
log::debug!("Router received new path: {route_match}");
})
.to_future();
parent.effect(wait_for_next_route);
parent.clone()
}
pub fn insert(&mut self, path: &str, page: fn(&App<S>) -> Dom) {
self.handler.insert(path.to_string(), page).unwrap();
}
pub fn set_not_found(&mut self, page: fn(&App<S>) -> Dom) {
self.not_found = Box::new(page);
}
pub fn handler(&self) -> matchit::Router<fn(&App<S>) -> Dom> {
self.handler.clone()
}
}
impl<S: Clone + 'static> Default for Router<S> {
fn default() -> Self {
Self::new()
}
}