pub mod context;
pub mod link;
pub use context::*;
pub use link::*;
use crate::router::context::{RouterContextProps, provide_router_context};
use silex_core::reactivity::{Effect, Signal, on_cleanup};
use silex_core::traits::{RxGet, RxWrite};
use silex_dom::view::{AnyView, View};
use silex_html::div;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure;
use web_sys::Event;
pub trait Routable: Sized + Clone + PartialEq + 'static {
fn match_path(path: &str) -> Option<Self>;
fn to_path(&self) -> String;
}
pub trait ToRoute {
fn to_route(&self) -> String;
}
impl ToRoute for &str {
fn to_route(&self) -> String {
self.to_string()
}
}
impl ToRoute for String {
fn to_route(&self) -> String {
self.clone()
}
}
impl ToRoute for &String {
fn to_route(&self) -> String {
self.to_string()
}
}
impl<R: Routable> ToRoute for R {
fn to_route(&self) -> String {
self.to_path()
}
}
#[derive(Clone)]
pub struct Router {
base_path: String,
child: Option<Rc<dyn Fn() -> AnyView>>,
}
impl Default for Router {
fn default() -> Self {
Self::new()
}
}
impl Router {
pub fn new() -> Self {
Self {
base_path: "/".to_string(),
child: None,
}
}
pub fn base(mut self, path: &str) -> Self {
let mut p = path.to_string();
if !p.starts_with('/') {
p = format!("/{}", p);
}
if p.len() > 1 && p.ends_with('/') {
p.pop();
}
self.base_path = p;
self
}
pub fn render<F, V>(mut self, view_fn: F) -> Self
where
V: View + Clone + 'static,
F: Fn() -> V + 'static,
{
self.child = Some(Rc::new(move || view_fn().into_any()));
self
}
pub fn match_enum<R, F, V>(mut self, render: F) -> Self
where
R: Routable,
F: Fn(R) -> V + 'static,
V: View + Clone + 'static,
{
self.child = Some(Rc::new(move || {
let path_signal = crate::router::use_location_path();
let path = path_signal.get();
if let Some(matched) = R::match_path(&path) {
render(matched).into_any()
} else {
AnyView::new(())
}
}));
self
}
pub fn match_route<R>(mut self) -> Self
where
R: RouteView,
{
self.child = Some(Rc::new(move || {
let path_signal = crate::router::use_location_path();
let path = path_signal.get();
if let Some(matched) = R::match_path(&path) {
matched.render()
} else {
AnyView::new(())
}
}));
self
}
}
pub trait RouteView: Routable {
fn render(&self) -> AnyView;
}
impl View for Router {
fn mount(self, parent: &web_sys::Node, attrs: Vec<silex_dom::attribute::PendingAttribute>) {
self.mount_internal(parent, attrs);
}
fn mount_ref(
&self,
parent: &web_sys::Node,
attrs: Vec<silex_dom::attribute::PendingAttribute>,
) {
self.clone().mount_internal(parent, attrs);
}
}
impl Router {
fn mount_internal(
self,
parent: &web_sys::Node,
attrs: Vec<silex_dom::attribute::PendingAttribute>,
) {
let window = web_sys::window().expect("no global `window` exists");
let location = window.location();
let raw_path = location.pathname().unwrap_or_else(|_| "/".into());
let initial_search = location.search().unwrap_or_else(|_| "".into());
let base_path = self.base_path.clone();
let initial_path =
if !base_path.is_empty() && base_path != "/" && raw_path.starts_with(&base_path) {
let p = &raw_path[base_path.len()..];
if p.is_empty() {
"/".to_string()
} else {
p.to_string()
}
} else {
raw_path
};
let (path, set_path) = Signal::pair(initial_path);
let (search, set_search) = Signal::pair(initial_search);
provide_router_context(RouterContextProps {
base_path: base_path.clone(),
path,
search,
set_path,
set_search,
});
let set_path_clone = set_path;
let set_search_clone = set_search;
let base_path_clone = base_path.clone();
let on_popstate = Closure::wrap(Box::new(move |_e: Event| {
let win = web_sys::window().unwrap();
let loc = win.location();
if let Ok(raw_p) = loc.pathname() {
let p = if !base_path_clone.is_empty()
&& base_path_clone != "/"
&& raw_p.starts_with(&base_path_clone)
{
let s = &raw_p[base_path_clone.len()..];
if s.is_empty() {
"/".to_string()
} else {
s.to_string()
}
} else {
raw_p
};
set_path_clone.set(p);
}
if let Ok(s) = loc.search() {
set_search_clone.set(s);
}
}) as Box<dyn FnMut(Event)>);
window
.add_event_listener_with_callback("popstate", on_popstate.as_ref().unchecked_ref())
.unwrap();
let container = div(());
let container_node = container.dom_element.clone();
container.mount(parent, attrs);
on_cleanup(move || {
let w = web_sys::window().unwrap();
let _ = w.remove_event_listener_with_callback(
"popstate",
on_popstate.as_ref().unchecked_ref(),
);
});
if let Some(child_factory) = self.child {
let parent = container_node.clone();
let factory = child_factory.clone();
Effect::new(move |_| {
parent.set_text_content(Some(""));
let view = factory();
view.mount(&parent, Vec::new());
});
}
}
}