use crate::log;
use std::borrow::Cow;
use crate::history::{AnyHistory, BrowserHistory, History, HistoryError, HistoryResult};
use crate::prelude::*;
use crate::use_context;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use yew_router::prelude::Location;
use gloo_net::http::Request;
use web_sys::{EventListener, RequestCache};
use wasm_bindgen_futures::spawn_local;
use web_sys::js_sys::Function;
pub type NavigationError = HistoryError;
pub type NavigationResult<T> = HistoryResult<T>;
#[derive(Clone)]
pub struct LocationContext {
location: Location,
ctr: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ComponentInfo {
pub component: Html,
pub err: &'static str,
}
impl LocationContext {
pub fn location(&self) -> Location {
self.location.clone()
}
}
impl PartialEq for LocationContext {
fn eq(&self, rhs: &Self) -> bool {
self.ctr == rhs.ctr
}
}
impl Reducible for LocationContext {
type Action = Location;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
Self {
location: action,
ctr: self.ctr + 1,
}
.into()
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct RouterProps {
#[prop_or_default]
pub children: Html,
#[prop_or(AnyHistory::Browser(BrowserHistory::new()))]
pub history: AnyHistory,
#[prop_or_default]
pub basename: &'static str,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum RouterKind {
Browser,
Hash,
Memory,
}
#[derive(Clone, PartialEq)]
pub struct RouterContext {
router: Router,
}
impl RouterContext {
pub fn router(&self) -> Router {
self.router.clone()
}
}
#[derive(Debug, Clone)]
pub struct Router {
history: AnyHistory,
basename: &'static str,
route: &'static str,
components: HashMap<&'static str, ComponentInfo>,
fetching_routes: HashSet<String>,
events: EventListener,
error_component: Html,
pathname: &'static str,
query: Value,
as_path: &'static str,
subscriptions: Vec<Callback<ComponentInfo>>,
component_load_cancel: Callback<()>,
}
impl PartialEq for Router {
fn eq(&self, other: &Self) -> bool {
self.history == other.history
&& self.basename == other.basename
&& self.route == other.route
&& self.components == other.components
&& self.fetching_routes.len() == other.fetching_routes.len()
&& self.events == other.events
&& self.error_component == other.error_component
&& self.pathname == other.pathname
&& self.query == other.query
&& self.as_path == other.as_path
&& self.subscriptions.len() == other.subscriptions.len()
&& self.component_load_cancel == other.component_load_cancel
}
}
impl Router {
pub fn new(
history: AnyHistory,
basename: &'static str,
route: &'static str,
components: HashMap<&'static str, ComponentInfo>,
fetching_routes: HashSet<String>,
events: EventListener,
error_component: Html,
pathname: &'static str,
query: Value,
as_path: &'static str,
subscriptions: Vec<Callback<ComponentInfo>>,
component_load_cancel: Callback<()>,
) -> Self {
Self {
history,
basename,
route,
components,
fetching_routes,
events,
error_component,
pathname,
query,
as_path,
subscriptions,
component_load_cancel,
}
}
pub fn basename(&self) -> &'static str {
self.basename
}
pub fn back(&self) {
self.go(-1);
}
pub fn forward(&self) {
self.go(1);
}
pub fn go(&self, delta: isize) {
self.history.go(delta);
}
pub fn push(&mut self, route: &'static str) {
self.route = route;
self.history.push(self.prefix_basename(route));
}
pub fn replace(&mut self, route: &'static str) {
self.route = route;
self.history.replace(self.prefix_basename(route));
}
pub fn push_with_state(&mut self, route: &'static str, state: &'static str) {
self.route = route;
self.history
.push_with_state(self.prefix_basename(route), state);
}
pub fn replace_with_state(&mut self, route: &'static str, state: &'static str) {
self.route = route;
self.history
.replace_with_state(self.prefix_basename(route), state);
}
pub fn push_with_query(&mut self, route: &'static str, query: &Value) -> NavigationResult<()> {
self.route = route;
self.query = query.clone();
self.history
.push_with_query(self.prefix_basename(route), query)
}
pub fn push_with_query_and_state(
&mut self,
route: &'static str,
query: &Value,
state: &'static str,
) -> NavigationResult<()> {
self.route = route;
self.query = query.clone();
self.history
.push_with_query_and_state(self.prefix_basename(route), query, state)
}
pub fn replace_with_query_and_state(
&mut self,
route: &'static str,
query: &Value,
state: Value,
) -> NavigationResult<()> {
self.route = route;
self.query = query.clone();
self.history
.replace_with_query_and_state(self.prefix_basename(route), query, state)
}
pub fn kind(&self) -> RouterKind {
match &self.history {
AnyHistory::Browser(_) => RouterKind::Browser,
AnyHistory::Hash(_) => RouterKind::Hash,
AnyHistory::Memory(_) => RouterKind::Memory,
}
}
pub fn prefix_basename<'a>(&self, route_s: &'a str) -> Cow<'a, str> {
let base = self.basename();
if !base.is_empty() {
if route_s.is_empty() && route_s.is_empty() {
Cow::from("/")
} else {
Cow::from(format!("{base}{route_s}"))
}
} else {
route_s.into()
}
}
pub fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> {
let m = self.basename();
if !m.is_empty() {
let mut path = path
.strip_prefix(m)
.map(|m| Cow::from(m.to_owned()))
.unwrap_or(path);
if !path.starts_with('/') {
path = format!("/{m}").into();
}
path
} else {
path
}
}
pub fn prefetch(&mut self, url: &'static str) {
self.fetch_route(url.to_string());
}
async fn fetch_gloo_net(url: &str) -> Result<ComponentInfo, Value> {
let response = match Request::get(url).cache(RequestCache::Reload).send().await {
Ok(res) => res,
Err(err) => {
return Err(err.to_string().into());
}
};
let _json_result = match response.json::<serde_json::Value>().await {
Ok(data) => data,
Err(err) => {
return Err(err.to_string().into());
}
};
Ok(ComponentInfo {
component: rsx! {},
err: "",
})
}
fn fetch_route(&mut self, route: String) {
let url = format!("/{}/index.json", route);
let events = EventListener::new();
let subscriptions = self.subscriptions.clone();
let as_path = self.as_path;
let route = route.clone();
let self_route = self.route;
let fetching_routes = Callback::from(move |_: String| {
let url = url.clone();
let mut fetching_routes = HashSet::new();
let mut events = events.clone();
let subscriptions = subscriptions.clone();
let as_path = as_path;
let route = route.clone();
let self_route = self_route;
spawn_local(async move {
let result = match Self::fetch_gloo_net(&url).await {
Ok(component_info) => {
fetching_routes.insert(route.clone());
if self_route == route {
if !component_info.err.is_empty() {
events.handle_event(&Function::new_with_args(
"route_change_error",
as_path,
));
}
Self::notify(subscriptions, component_info);
events.handle_event(&Function::new_with_args(
"route_change_complete",
as_path,
));
}
Ok(())
}
Err(fetch_error) => {
fetching_routes.insert(route.clone());
log(&format!("Error fetching route: {:?}", fetch_error).into());
if self_route == route {
let component_info = ComponentInfo {
component: rsx! {},
err: "Error fetching route",
};
Self::notify(subscriptions, component_info);
events.handle_event(&Function::new_with_args(
"route_change_complete",
as_path,
));
}
Err(fetch_error)
}
};
if let Err(error) = result {
log(&format!("Failed to handle fetch result: {:?}", error).into());
}
});
});
fetching_routes.emit("".to_string())
}
fn notify(subscriptions: Vec<Callback<ComponentInfo>>, data: ComponentInfo) {
subscriptions.iter().for_each(|callback| {
callback.emit(data.clone());
});
}
fn _subscribe(&mut self, callback: Callback<ComponentInfo>) -> Callback<()> {
self.subscriptions.push(callback.clone());
Callback::from(move |_| {
})
}
}
#[func]
pub fn BaseRouter(props: &RouterProps) -> Html {
let RouterProps {
history,
children,
basename,
} = props.clone();
let loc_ctx = use_reducer(|| LocationContext {
location: history.location(),
ctr: 0,
});
let trigger = use_force_update();
let prefetched_component = use_state(|| rsx! {<></>});
let component_value = (*prefetched_component).clone();
let basename = basename.strip_suffix('/').unwrap_or(basename);
let route = "/";
let components = HashMap::new();
let fetching_routes = HashSet::new();
let events = EventListener::new();
let error_component = Html::default();
let pathname = "";
let query = Value::default();
let as_path = "";
let mut subscriptions = Vec::new();
subscriptions.push(Callback::from(move |component: ComponentInfo| {
prefetched_component.set(component.component);
trigger.force_update();
log(&format!("prefetch callback...").into());
}));
let component_load_cancel = Callback::default();
let router = Router::new(
history.clone(),
basename,
route,
components,
fetching_routes,
events,
error_component,
pathname,
query,
as_path,
subscriptions,
component_load_cancel,
);
let navi_ctx = RouterContext {
router: router.clone(),
};
{
let loc_ctx_dispatcher = loc_ctx.dispatcher();
use_effect_with(history, move |history| {
let history = history.clone();
loc_ctx_dispatcher.dispatch(history.location());
let history_cb = {
let history = history.clone();
move || loc_ctx_dispatcher.dispatch(history.location())
};
let listener = history.listen(history_cb);
move || {
std::mem::drop(listener);
}
});
}
rsx! {
<ContextProvider<RouterContext> context={navi_ctx}>
<ContextProvider<LocationContext> context={(*loc_ctx).clone()}>
{children}
{component_value}
</ContextProvider<LocationContext>>
</ContextProvider<RouterContext>>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct SwitchProps {
pub render: Callback<String, Html>,
#[prop_or_default]
pub pathname: &'static str,
}
#[func]
pub fn Switch(props: &SwitchProps) -> Html {
let mut route = use_route();
if route.is_empty() {
route = std::borrow::Cow::Borrowed(props.pathname);
}
if !route.is_empty() {
props.render.emit(route.to_string())
} else {
Html::default()
}
}
#[func]
pub fn NextRouter(props: &RouterProps) -> Html {
rsx! {
<BaseRouter ..props.clone() />
}
}
#[hook]
pub fn use_router() -> Router {
use_context::<RouterContext>()
.map(|m| m.router())
.expect("router")
}
#[hook]
pub fn use_location() -> Option<Location> {
Some(use_context::<LocationContext>()?.location())
}
#[hook]
pub fn use_route() -> Cow<'static, str> {
let router = use_router();
let location = use_location().expect("location");
let stripped_path: Cow<'static, str> = router
.strip_basename(Cow::Borrowed(location.path()))
.into_owned()
.into();
stripped_path
}