use silex_core::reactivity::{Memo, ReadSignal, Signal, WriteSignal, provide_context, use_context};
use silex_core::traits::{RxGet, RxWrite};
use silex_dom::view::{AnyView, View};
use std::collections::HashMap;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use web_sys::Node;
#[derive(Clone)]
pub struct RouterViewFactory(pub Rc<dyn Fn() -> AnyView>);
impl PartialEq for RouterViewFactory {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl View for RouterViewFactory {
fn mount(self, parent: &Node, attrs: Vec<silex_dom::attribute::PendingAttribute>) {
let factory = self.0;
let closure = move || (factory)();
closure.mount(parent, attrs);
}
fn mount_ref(&self, parent: &Node, attrs: Vec<silex_dom::attribute::PendingAttribute>) {
self.clone().mount(parent, attrs);
}
}
#[derive(Clone)]
pub struct RouterContext {
pub base_path: String,
pub path: ReadSignal<String>,
pub search: ReadSignal<String>,
pub navigator: Navigator,
}
#[derive(Clone)]
pub struct Navigator {
pub(crate) base_path: String,
pub(crate) path: ReadSignal<String>,
pub(crate) search: ReadSignal<String>,
pub(crate) set_path: WriteSignal<String>,
pub(crate) set_search: WriteSignal<String>,
}
impl Navigator {
fn handle_navigation(&self, url: &str, replace: bool) {
let window = web_sys::window().unwrap();
let full_url = if url.starts_with('/') {
if self.base_path == "/" || self.base_path.is_empty() {
url.to_string()
} else {
let base = self.base_path.trim_end_matches('/');
format!("{}{}", base, url)
}
} else {
url.to_string()
};
if let Ok(history) = window.history() {
if replace {
let _ = history.replace_state_with_url(
&wasm_bindgen::JsValue::NULL,
"",
Some(&full_url),
);
} else {
let _ =
history.push_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&full_url));
}
}
let location = window.location();
let raw_path = location.pathname().unwrap_or_else(|_| "/".to_string());
let logical_path = if !self.base_path.is_empty()
&& self.base_path != "/"
&& raw_path.starts_with(&self.base_path)
{
let p = &raw_path[self.base_path.len()..];
if p.is_empty() { "/" } else { p }
} else {
&raw_path
};
let search = location.search().unwrap_or_default();
if self.path.get_untracked() != logical_path {
self.set_path.set(logical_path.to_string());
}
if self.search.get_untracked() != search {
self.set_search.set(search);
}
}
pub fn push<T: crate::router::ToRoute>(&self, to: T) {
self.handle_navigation(&to.to_route(), false);
}
pub fn replace<T: crate::router::ToRoute>(&self, to: T) {
self.handle_navigation(&to.to_route(), true);
}
pub fn set_query(&self, key: &str, value: Option<&str>) {
let current_search = self.search.get_untracked();
if let Ok(params) = web_sys::UrlSearchParams::new_with_str(¤t_search) {
match value {
Some(v) => params.set(key, v),
None => params.delete(key),
}
let new_search = params.to_string().as_string().unwrap_or_default();
let pathname = self.path.get_untracked();
let new_url = if new_search.is_empty() {
pathname
} else {
format!("{}?{}", pathname, new_search)
};
self.push(&new_url);
}
}
}
#[derive(Clone)]
pub(crate) struct RouterContextProps {
pub base_path: String,
pub path: ReadSignal<String>,
pub search: ReadSignal<String>,
pub set_path: WriteSignal<String>,
pub set_search: WriteSignal<String>,
}
pub(crate) fn provide_router_context(props: RouterContextProps) {
let navigator = Navigator {
base_path: props.base_path.clone(),
path: props.path,
search: props.search,
set_path: props.set_path,
set_search: props.set_search,
};
let ctx = RouterContext {
base_path: props.base_path,
path: props.path,
search: props.search,
navigator,
};
provide_context(ctx);
}
pub fn use_router() -> Option<RouterContext> {
use_context::<RouterContext>()
}
pub fn use_navigate() -> Navigator {
use_router()
.expect("use_navigate called outside of <Router>")
.navigator
}
pub fn use_location_path() -> Signal<String> {
use_router()
.map(|ctx| ctx.path.into())
.expect("use_location_path called outside of <Router>")
}
pub fn use_location_search() -> Signal<String> {
use_router()
.map(|ctx| ctx.search.into())
.expect("use_location called outside of <Router>")
}
pub fn use_query_map() -> silex_core::reactivity::Memo<HashMap<String, String>> {
let search_signal = use_location_search();
Memo::new(move |_| {
let s = search_signal.get();
let mut map = HashMap::new();
if let Ok(params) = web_sys::UrlSearchParams::new_with_str(&s) {
if let Ok(Some(iter)) = js_sys::try_iter(¶ms) {
for val in iter.flatten() {
let pair: js_sys::Array = val.unchecked_into();
let k = pair.get(0).as_string().unwrap_or_default();
let v = pair.get(1).as_string().unwrap_or_default();
map.insert(k, v);
}
}
}
map
})
}