diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..e432f24 --- /dev/null +++ b/src/history.rs @@ -0,0 +1,68 @@ +use std::cell::RefCell; +use std::rc::{Rc, Weak}; +use wasm_bindgen::JsValue; + +thread_local! { + static INSTANCE: RefCell = RefCell::new(InnerHistory { + listeners: vec![], + }); +} + +/// Handle to a history listener. +/// +/// Disposes the listener when dropped. +pub struct HistoryListener(Rc); + +pub struct History; + +impl History { + /// Subscribe to events of the browser history. + /// + /// This will receive events when popping items from the stack, as well as changes triggered by calling + /// [`History::push_state`]. + #[must_use = "The listener will only be active for as long as the returned instance exists."] + pub fn listener(f: F) -> HistoryListener { + INSTANCE.with(|instance| instance.borrow_mut().listener(f)) + } + + /// Push a new state to the history stack. + /// + /// This will send out events and update the browser's history. Ultimately calling + /// [`web_sys::History::push_state_with_url`]. + pub fn push_state(state: JsValue, url: &str) -> Result<(), JsValue> { + INSTANCE.with(|instance| instance.borrow_mut().push_state(state, url)) + } +} + +type CallbackFn = dyn Fn() + 'static; + +struct InnerHistory { + listeners: Vec>, +} + +impl InnerHistory { + fn push_state(&mut self, state: JsValue, url: &str) -> Result<(), JsValue> { + let result = gloo_utils::history().push_state_with_url(&state, "", Some(url)); + self.notify(); + result + } + + fn listener(&mut self, f: F) -> HistoryListener { + let callback = Rc::new(f) as Rc; + self.listeners.push(Rc::downgrade(&callback)); + HistoryListener(callback) + } + + fn notify(&mut self) { + let mut new = vec![]; + + for listener in &mut self.listeners { + if let Some(cb) = listener.upgrade() { + (*cb)(); + new.push(listener.clone()); + } + } + + self.listeners = new; + } +} diff --git a/src/lib.rs b/src/lib.rs index 5a410ce..72bc14b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,6 +223,15 @@ //! } //! ``` //! +//! ## Interoperability +//! +//! This implementation makes use of the browser's history API. While it is possible to receive the current state +//! from the History API, and trigger "back" operations, using [`web_sys::History::push_state`] directly will not +//! trigger an event and thus no render a different page. +//! +//! As `gloo_history` creates its internal type and state system, it is not interoperable with this crate. It still is +//! possible to use [`gloo_utils::history`] though, which is just a shortcut of getting [`web_sys::History`]. +//! //! ## More examples //! //! See the `examples` folder. @@ -231,6 +240,7 @@ pub mod components; pub mod target; mod base; +mod history; mod router; mod scope; mod state; diff --git a/src/router.rs b/src/router.rs index af09e7b..0e03ba5 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,8 +1,8 @@ use crate::base; +use crate::history::{History, HistoryListener}; use crate::scope::{NavigationTarget, ScopeContext}; use crate::state::State; use crate::target::Target; -use gloo_events::EventListener; use gloo_utils::window; use std::borrow::Cow; use std::fmt::Debug; @@ -159,7 +159,7 @@ pub enum Msg { /// Top-level router component. pub struct Router { - _listener: EventListener, + _listener: HistoryListener, target: Option, scope: Rc>, @@ -189,8 +189,8 @@ where let target = Self::parse_location(&base, window().location()) .or_else(|| ctx.props().default.clone()); - let listener = EventListener::new(&window(), "popstate", move |_| { - cb.emit(gloo_utils::window().location()); + let listener = History::listener(move || { + cb.emit(window().location()); }); let (scope, router) = Self::build_context(base.clone(), &target, ctx); @@ -205,8 +205,6 @@ where } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - // log::debug!("update: {msg:?}"); - match msg { Msg::RouteChanged => { let target = Self::parse_location(&self.base, window().location()) @@ -219,9 +217,7 @@ where } Msg::ChangeTarget(target) => { let route = Self::render_target(&self.base, &target.target); - // log::debug!("Push URL: {route}"); - // log::debug!("Push State: {:?}", target.state); - let _ = gloo_utils::history().push_state_with_url(&target.state, "", Some(&route)); + let _ = History::push_state(target.state, &route); ctx.link().send_message(Msg::RouteChanged) } }