pub mod context;
pub mod link;
pub use context::*;
pub use link::*;
use crate::router::context::{RouterContextProps, provide_router_context};
use silex_core::reactivity::{Signal, on_cleanup};
use silex_core::traits::{RxGet, RxWrite};
use silex_dom::attribute::PendingAttribute;
use silex_dom::view::{AnyView, ApplyAttributes, View};
use silex_macros::component;
use std::marker::PhantomData;
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()
}
}
#[component]
pub fn Router(
#[prop(into)]
#[chain(default = "/")]
base: String,
#[prop(render)]
#[chain(default = AnyView::Empty)]
children: AnyView,
) -> impl View {
RouterView {
base_path: normalize_base_path(&base),
children,
}
}
impl RouterComponent {
pub fn match_route<R>(mut self) -> Self
where
R: RouteView + 'static,
{
self.children = RouterRouteView::<R>::new().into_any();
self
}
pub fn match_enum<R, F, V>(mut self, render: F) -> Self
where
R: Routable + 'static,
F: Fn(R) -> V + Clone + 'static,
V: View + 'static,
{
self.children = RouterMatchView::<R, F, V>::new(render).into_any();
self
}
}
fn normalize_base_path(path: &str) -> String {
let mut p = path.to_string();
if !p.starts_with('/') {
p = format!("/{}", p);
}
if p.len() > 1 && p.ends_with('/') {
p.pop();
}
p
}
#[derive(Clone)]
struct RouterView {
base_path: String,
children: AnyView,
}
impl ApplyAttributes for RouterView {}
impl View for RouterView {
fn mount(&self, parent: &web_sys::Node, attrs: Vec<PendingAttribute>) {
self.clone().mount_owned(parent, attrs);
}
fn mount_owned(self, parent: &web_sys::Node, attrs: Vec<PendingAttribute>)
where
Self: Sized,
{
self.mount_internal(parent, attrs);
}
}
impl RouterView {
fn mount_internal(self, parent: &web_sys::Node, attrs: Vec<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();
on_cleanup(move || {
let w = web_sys::window().unwrap();
let _ = w.remove_event_listener_with_callback(
"popstate",
on_popstate.as_ref().unchecked_ref(),
);
});
self.children.mount_owned(parent, attrs);
}
}
#[derive(Clone)]
struct RouterRouteView<R> {
_phantom: PhantomData<R>,
}
impl<R> RouterRouteView<R> {
fn new() -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<R> ApplyAttributes for RouterRouteView<R> {}
impl<R> View for RouterRouteView<R>
where
R: RouteView + 'static,
{
fn mount(&self, parent: &web_sys::Node, attrs: Vec<PendingAttribute>) {
let path_signal = crate::router::use_location_path();
silex_dom::view::mount_branch_cached(
parent,
attrs,
move || path_signal.get(),
move |path| {
if let Some(matched) = R::match_path(&path) {
matched.render()
} else {
AnyView::Empty
}
},
);
}
fn mount_owned(self, parent: &web_sys::Node, attrs: Vec<PendingAttribute>)
where
Self: Sized,
{
let path_signal = crate::router::use_location_path();
silex_dom::view::mount_branch_cached(
parent,
attrs,
move || path_signal.get(),
move |path| {
if let Some(matched) = R::match_path(&path) {
matched.render()
} else {
AnyView::Empty
}
},
);
}
}
#[derive(Clone)]
struct RouterMatchView<R, F, V> {
render: Rc<F>,
_phantom: PhantomData<(R, V)>,
}
impl<R, F, V> RouterMatchView<R, F, V> {
fn new(render: F) -> Self {
Self {
render: Rc::new(render),
_phantom: PhantomData,
}
}
}
impl<R, F, V> ApplyAttributes for RouterMatchView<R, F, V> {}
impl<R, F, V> View for RouterMatchView<R, F, V>
where
R: Routable + 'static,
F: Fn(R) -> V + Clone + 'static,
V: View + 'static,
{
fn mount(&self, parent: &web_sys::Node, attrs: Vec<PendingAttribute>) {
let path_signal = crate::router::use_location_path();
let render = self.render.clone();
silex_dom::view::mount_branch_cached(
parent,
attrs,
move || path_signal.get(),
move |path| {
if let Some(matched) = R::match_path(&path) {
render(matched).into_any()
} else {
AnyView::Empty
}
},
);
}
fn mount_owned(self, parent: &web_sys::Node, attrs: Vec<PendingAttribute>)
where
Self: Sized,
{
let path_signal = crate::router::use_location_path();
let render = self.render;
silex_dom::view::mount_branch_cached(
parent,
attrs,
move || path_signal.get(),
move |path| {
if let Some(matched) = R::match_path(&path) {
render(matched).into_any()
} else {
AnyView::Empty
}
},
);
}
}
pub trait RouteView: Routable {
fn render(&self) -> AnyView;
}