Skip to content

Added hashchange listener #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

## v0.4.2
- Added an `Init` struct, which can help with initial routing (Breaking)
- The `routes` function now returns an `Option<Msg>` (Breaking)
- updated `Tag::from()` to accept more input types
- Fixed a bug affecting element render order
- Improved error-handling
- Macro `custom!` checks if you set tag, and panics when you forget
- Fixed a bug with children being absent from cloned elements
- Improved debugging
- Added a routing listener for changed hash
- Fixed a namespace bug with adding children to `Svg` elements
- Fixed a bug affecting Safari

## v0.4.1
- Added more SVG `At` variants
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ features = [
"Element",
"Event",
"EventTarget",
"HashChangeEvent",
"Headers",
"History",
"HtmlElement",
Expand Down
154 changes: 136 additions & 18 deletions src/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ mod util {

/// Contains all information used in pushing and handling routes.
/// Based on [React-Reason's router](https://github.com/reasonml/reason-react/blob/master/docs/router.md).
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Url {
pub path: Vec<String>,
pub search: Option<String>,
Expand Down Expand Up @@ -74,14 +74,62 @@ impl Url {
}

impl From<String> for Url {
// todo: Include hash and search.
fn from(s: String) -> Self {
// This could be done more elegantly with regex, but adding it as a dependency
// causes a big increase in WASM size.
let mut path: Vec<String> = s.split('/').map(ToString::to_string).collect();
path.remove(0); // Remove a leading empty string.
if let Some(first) = path.get(0) {
if first.is_empty() {
path.remove(0); // Remove a leading empty string.
}
}

// We assume hash and search terms are at the end of the path, and hash comes before search.
let last = path.pop();

let mut last_path = String::new();
let mut hash = String::new();
let mut search = String::new();

if let Some(l) = last {
let mut in_hash = false;
let mut in_search = false;
for c in l.chars() {
if c == '#' {
in_hash = true;
in_search = false;
continue;
}

if c == '?' {
in_hash = false;
in_search = true;
continue;
}

if in_hash {
hash.push(c);
} else if in_search {
search.push(c);
} else {
last_path.push(c);
}
}
}

// Re-add the pre-`#` and pre-`?` part of the path.
if !last_path.is_empty() {
path.push(last_path);
}

Self {
path,
hash: None,
search: None,
hash: if hash.is_empty() { None } else { Some(hash) },
search: if search.is_empty() {
None
} else {
Some(search)
},
title: None,
}
}
Expand Down Expand Up @@ -214,7 +262,7 @@ pub fn push_route<U: Into<Url>>(url: U) -> Url {
/// Add a listener that handles routing for navigation events like forward and back.
pub fn setup_popstate_listener<Ms>(
update: impl Fn(Ms) + 'static,
update_ps_listener: impl Fn(Closure<dyn FnMut(web_sys::Event)>) + 'static,
updated_listener: impl Fn(Closure<dyn FnMut(web_sys::Event)>) + 'static,
routes: fn(Url) -> Option<Ms>,
) where
Ms: 'static,
Expand All @@ -224,27 +272,49 @@ pub fn setup_popstate_listener<Ms>(
.dyn_ref::<web_sys::PopStateEvent>()
.expect("Problem casting as Popstate event");

let url: Url = match ev.state().as_string() {
Some(state_str) => {
serde_json::from_str(&state_str).expect("Problem deserializing popstate state")
}
// This might happen if we go back to a page before we started routing. (?)
None => {
let empty: Vec<String> = Vec::new();
Url::new(empty)
if let Some(state_str) = ev.state().as_string() {
let url: Url =
serde_json::from_str(&state_str).expect("Problem deserializing popstate state");
// Only update when requested for an update by the user.
if let Some(routing_msg) = routes(url) {
update(routing_msg);
}
};
// Only update when requested for an update by the user.
});

(util::window().as_ref() as &web_sys::EventTarget)
.add_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref())
.expect("Problem adding popstate listener");

updated_listener(closure);
}

/// Add a listener that handles routing when the url hash is changed.
pub fn setup_hashchange_listener<Ms>(
update: impl Fn(Ms) + 'static,
updated_listener: impl Fn(Closure<dyn FnMut(web_sys::Event)>) + 'static,
routes: fn(Url) -> Option<Ms>,
) where
Ms: 'static,
{
// todo: DRY with popstate listener
let closure = Closure::new(move |ev: web_sys::Event| {
let ev = ev
.dyn_ref::<web_sys::HashChangeEvent>()
.expect("Problem casting as hashchange event");

let url: Url = ev.new_url().into();

if let Some(routing_msg) = routes(url) {
update(routing_msg);
}
});

(util::window().as_ref() as &web_sys::EventTarget)
.add_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref())
.expect("Problem adding popstate listener");
.add_event_listener_with_callback("hashchange", closure.as_ref().unchecked_ref())
.expect("Problem adding hashchange listener");

update_ps_listener(closure);
updated_listener(closure);
}

/// Set up a listener that intercepts clicks on elements containing an Href attribute,
Expand Down Expand Up @@ -299,3 +369,51 @@ where

closure.forget(); // todo: Can we store the closure somewhere to avoid using forget?
}

#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;

use super::*;

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
fn parse_url_simple() {
let expected = Url {
path: vec!["path1".into(), "path2".into()],
hash: None,
search: None,
title: None,
};

let actual: Url = "/path1/path2".to_string().into();
assert_eq!(expected, actual)
}

#[wasm_bindgen_test]
fn parse_url_with_hash_search() {
let expected = Url {
path: vec!["path".into()],
hash: Some("hash".into()),
search: Some("sea=rch".into()),
title: None,
};

let actual: Url = "/path/#hash?sea=rch".to_string().into();
assert_eq!(expected, actual)
}

#[wasm_bindgen_test]
fn parse_url_with_hash_only() {
let expected = Url {
path: vec!["path".into()],
hash: Some("hash".into()),
search: None,
title: None,
};

let actual: Url = "/path/#hash".to_string().into();
assert_eq!(expected, actual)
}
}
13 changes: 12 additions & 1 deletion src/vdom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ pub struct AppData<Ms: 'static + Clone, Mdl> {
pub model: RefCell<Option<Mdl>>,
main_el_vdom: RefCell<Option<El<Ms>>>,
pub popstate_closure: StoredPopstate,
pub hashchange_closure: StoredPopstate,
pub routes: RefCell<Option<RoutesFn<Ms>>>,
window_listeners: RefCell<Vec<events::Listener<Ms>>>,
msg_listeners: RefCell<MsgListeners<Ms>>,
Expand Down Expand Up @@ -283,7 +284,9 @@ impl<Ms: Clone, Mdl, ElC: View<Ms> + 'static, GMs: 'static> AppBuilder<Ms, Mdl,
UrlHandling::PassToRoutes => {
let url = routing::initial_url();
if let Some(r) = self.routes {
(self.update)(r(url).unwrap(), &mut init.model, &mut initial_orders);
if let Some(u) = r(url) {
(self.update)(u, &mut init.model, &mut initial_orders);
}
}
}
UrlHandling::None => (),
Expand Down Expand Up @@ -345,6 +348,7 @@ impl<Ms: Clone, Mdl, ElC: View<Ms> + 'static, GMs: 'static> App<Ms, Mdl, ElC, GM
// This is filled for the first time in run()
main_el_vdom: RefCell::new(None),
popstate_closure: RefCell::new(None),
hashchange_closure: RefCell::new(None),
routes: RefCell::new(routes),
window_listeners: RefCell::new(Vec::new()),
msg_listeners: RefCell::new(Vec::new()),
Expand Down Expand Up @@ -418,6 +422,13 @@ impl<Ms: Clone, Mdl, ElC: View<Ms> + 'static, GMs: 'static> App<Ms, Mdl, ElC, GM
}),
routes,
);
routing::setup_hashchange_listener(
enclose!((self => s) move |msg| s.update(msg)),
enclose!((self => s) move |closure| {
s.data.hashchange_closure.replace(Some(closure));
}),
routes,
);
routing::setup_link_listener(enclose!((self => s) move |msg| s.update(msg)), routes);
}
self
Expand Down