浏览代码

feat: router crate is polished up

Jonathan Kelley 3 年之前
父节点
当前提交
cc83900334

+ 2 - 2
packages/router/Cargo.toml

@@ -31,7 +31,7 @@ web-sys = { version = "0.3", features = [
 ], optional = true }
 wasm-bindgen = { version = "0.2", optional = true }
 js-sys = { version = "0.3", optional = true }
-gloo = { version = "0.6", optional = true }
+gloo-events = { version = "0.1.1", optional = true }
 log = "0.4.14"
 thiserror = "1.0.30"
 futures-util = "0.3.21"
@@ -39,7 +39,7 @@ futures-util = "0.3.21"
 
 [features]
 default = ["web"]
-web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"]
+web = ["web-sys", "gloo-events", "js-sys", "wasm-bindgen"]
 hash = []
 
 [dev-dependencies]

+ 6 - 1
packages/router/examples/simple.rs

@@ -24,6 +24,8 @@ fn app(cx: Scope) -> Element {
             Route { to: "/", Home {} }
             Route { to: "/blog/", BlogList {} }
             Route { to: "/blog/:id/", BlogPost {} }
+            Route { to: "/oranges", "Oranges are not apples!" }
+            Redirect { from: "/apples", to: "/oranges" }
         }
     })
 }
@@ -41,6 +43,9 @@ fn BlogList(cx: Scope) -> Element {
 }
 
 fn BlogPost(cx: Scope) -> Element {
-    let id = use_route(&cx).segment::<usize>("id")?;
+    let id = use_route(&cx).segment("id")?;
+
+    log::debug!("rendering blog post {}", id);
+
     cx.render(rsx! { div { "{id:?}" } })
 }

+ 1 - 1
packages/router/src/components/link.rs

@@ -101,7 +101,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
             onclick: move |_| {
                 if !outerlink {
                     if let Some(service) = svc {
-                        service.push_route(to);
+                        service.push_route(to, cx.props.title.map(|f| f.to_string()), None);
                     } else {
                         log::error!(
                             "Attempted to create a Link to {} outside of a Router context",

+ 39 - 3
packages/router/src/components/redirect.rs

@@ -1,8 +1,44 @@
+use dioxus_core as dioxus;
 use dioxus_core::prelude::*;
+use dioxus_core_macro::Props;
 
+use crate::use_router;
+
+/// The props for the [`Router`](fn.Router.html) component.
+#[derive(Props)]
+pub struct RedirectProps<'a> {
+    /// The route to link to. This can be a relative path, or a full URL.
+    ///
+    /// ```rust
+    /// // Absolute path
+    /// Redirect { from: "", to: "/home" }
+    ///
+    /// // Relative path
+    /// Redirect { from: "", to: "../" }
+    /// ```
+    pub to: &'a str,
+
+    /// The route to link to. This can be a relative path, or a full URL.
+    ///
+    /// ```rust
+    /// // Absolute path
+    /// Redirect { from: "", to: "/home" }
+    ///
+    /// // Relative path
+    /// Redirect { from: "", to: "../" }
+    /// ```
+    #[props(optional)]
+    pub from: Option<&'a str>,
+}
+
+/// If this component is rendered, it will redirect the user to the given route.
+///
+/// It will replace the current route rather than pushing the current one to the stack.
+pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element {
+    let router = use_router(&cx);
+
+    // todo: check if the current location matches the "from" pattern
+    router.replace_route(cx.props.to, None, None);
 
-// The entire point of this component is to immediately redirect to the given path.
-pub fn Redirect(cx: Scope) -> Element {
-    //
     None
 }

+ 53 - 24
packages/router/src/components/router.rs

@@ -1,19 +1,11 @@
-use crate::location::ParsedRoute;
-use std::cell::Cell;
-use std::cell::RefCell;
-use std::collections::HashMap;
-use std::rc::Rc;
-use std::sync::Arc;
-
-use crate::cfg::RouterCfg;
-use crate::RouteEvent;
-use crate::RouterCore;
+use crate::ParsedRoute;
+use crate::{cfg::RouterCfg, RouteEvent, RouterCore};
 use dioxus_core as dioxus;
 use dioxus_core::prelude::*;
-use dioxus_core::Element;
 use dioxus_core_macro::*;
 use dioxus_html as dioxus_elements;
 use futures_util::stream::StreamExt;
+use std::sync::Arc;
 
 /// The props for the [`Router`](fn.Router.html) component.
 #[derive(Props)]
@@ -45,8 +37,6 @@ pub struct RouterProps<'a> {
 /// Will fallback to HashRouter is BrowserRouter is not available, or through configuration.
 #[allow(non_snake_case)]
 pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
-    let call_onchange = cx.use_hook(|_| Rc::new(Cell::new(false)));
-
     let svc = cx.use_hook(|_| {
         let (tx, mut rx) = futures_channel::mpsc::unbounded::<RouteEvent>();
 
@@ -57,24 +47,63 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
         cx.spawn({
             let svc = svc.clone();
             let regen_route = cx.schedule_update_any();
-            let call_onchange = call_onchange.clone();
             let router_id = cx.scope_id();
 
             async move {
                 while let Some(msg) = rx.next().await {
-                    if let Some(_new) = svc.handle_route_event(msg) {
-                        call_onchange.set(true);
-
-                        regen_route(router_id);
+                    match msg {
+                        RouteEvent::Push {
+                            route,
+                            serialized_state,
+                            title,
+                        } => {
+                            let new_route = Arc::new(ParsedRoute {
+                                url: svc.current_location().url.join(&route).ok().unwrap(),
+                                title,
+                                serialized_state,
+                            });
+
+                            svc.history.push(&new_route);
+                            svc.stack.borrow_mut().push(new_route);
+                        }
 
-                        for listener in svc.onchange_listeners.borrow().iter() {
-                            regen_route(*listener);
+                        RouteEvent::Replace {
+                            route,
+                            title,
+                            serialized_state,
+                        } => {
+                            let new_route = Arc::new(ParsedRoute {
+                                url: svc.current_location().url.join(&route).ok().unwrap(),
+                                title,
+                                serialized_state,
+                            });
+
+                            svc.history.replace(&new_route);
+                            *svc.stack.borrow_mut().last_mut().unwrap() = new_route;
                         }
 
-                        for route in svc.slots.borrow().keys() {
-                            regen_route(*route);
+                        RouteEvent::Pop => {
+                            let mut stack = svc.stack.borrow_mut();
+
+                            if stack.len() == 1 {
+                                continue;
+                            }
+
+                            stack.pop();
                         }
                     }
+
+                    svc.route_found.set(None);
+
+                    regen_route(router_id);
+
+                    for listener in svc.onchange_listeners.borrow().iter() {
+                        regen_route(*listener);
+                    }
+
+                    for route in svc.slots.borrow().keys() {
+                        regen_route(*route);
+                    }
                 }
             }
         });
@@ -82,9 +111,9 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
         cx.provide_context(svc)
     });
 
-    if call_onchange.get() {
+    // next time we run the rout_found will be filled
+    if svc.route_found.get().is_none() {
         cx.props.onchange.call(svc.clone());
-        call_onchange.set(false);
     }
 
     cx.render(rsx!(&cx.props.children))

+ 0 - 5
packages/router/src/hooks/use_param.rs

@@ -1,5 +0,0 @@
-use dioxus_core::prelude::*;
-
-pub fn use_param<T>(cx: &ScopeState) -> Option<T> {
-    todo!()
-}

+ 0 - 0
packages/router/src/hooks/use_query.rs


+ 81 - 8
packages/router/src/hooks/use_route.rs

@@ -1,27 +1,100 @@
+use crate::{ParsedRoute, RouteContext, RouterCore, RouterService};
 use dioxus_core::{ScopeId, ScopeState};
-use std::{rc::Rc, str::FromStr, sync::Arc};
-
-use crate::{location::ParsedRoute, RouterCore, RouterService};
+use std::{borrow::Cow, str::FromStr, sync::Arc};
+use url::Url;
 
 /// This hook provides access to information about the current location in the
 /// context of a [`Router`]. If this function is called outside of a `Router`
 /// component it will panic.
-pub fn use_route(cx: &ScopeState) -> &ParsedRoute {
+pub fn use_route(cx: &ScopeState) -> &UseRoute {
     let handle = cx.use_hook(|_| {
         let router = cx
-            .consume_context::<Arc<RouterCore>>()
+            .consume_context::<RouterService>()
+            .expect("Cannot call use_route outside the scope of a Router component");
+
+        let route_context = cx
+            .consume_context::<RouteContext>()
             .expect("Cannot call use_route outside the scope of a Router component");
 
         router.subscribe_onchange(cx.scope_id());
 
         UseRouteListener {
-            route: router.current_location(),
+            state: UseRoute {
+                route_context,
+                route: router.current_location(),
+            },
             router,
             scope: cx.scope_id(),
         }
     });
 
-    &handle.route
+    handle.state.route = handle.router.current_location();
+
+    &handle.state
+}
+
+/// A handle to the current location of the router.
+pub struct UseRoute {
+    pub(crate) route: Arc<ParsedRoute>,
+    pub(crate) route_context: RouteContext,
+}
+
+impl UseRoute {
+    /// Get the underlying [`Url`] of the current location.
+    pub fn url(&self) -> &Url {
+        &self.route.url
+    }
+
+    /// Get the first query parameter given the parameter name.
+    ///
+    /// If you need to get more than one parameter, use [`query_pairs`] on the [`Url`] instead.
+    pub fn query(&self, param: &str) -> Option<Cow<str>> {
+        self.route
+            .url
+            .query_pairs()
+            .find(|(k, _)| k == param)
+            .map(|(_, v)| v)
+    }
+
+    /// Returns the nth segment in the path. Paths that end with a slash have
+    /// the slash removed before determining the segments. If the path has
+    /// fewer segments than `n` then this method returns `None`.
+    pub fn nth_segment(&self, n: usize) -> Option<&str> {
+        self.route.url.path_segments()?.nth(n)
+    }
+
+    /// Returns the last segment in the path. Paths that end with a slash have
+    /// the slash removed before determining the segments. The root path, `/`,
+    /// will return an empty string.
+    pub fn last_segment(&self) -> Option<&str> {
+        self.route.url.path_segments()?.last()
+    }
+
+    /// Get the named parameter from the path, as defined in your router. The
+    /// value will be parsed into the type specified by `T` by calling
+    /// `value.parse::<T>()`. This method returns `None` if the named
+    /// parameter does not exist in the current path.
+    pub fn segment(&self, name: &str) -> Option<&str> {
+        let index = self
+            .route_context
+            .total_route
+            .trim_start_matches('/')
+            .split('/')
+            .position(|segment| segment.starts_with(':') && &segment[1..] == name)?;
+
+        self.route.url.path_segments()?.nth(index)
+    }
+
+    /// Get the named parameter from the path, as defined in your router. The
+    /// value will be parsed into the type specified by `T` by calling
+    /// `value.parse::<T>()`. This method returns `None` if the named
+    /// parameter does not exist in the current path.
+    pub fn parse_segment<T>(&self, name: &str) -> Option<Result<T, T::Err>>
+    where
+        T: FromStr,
+    {
+        self.segment(name).map(|value| value.parse::<T>())
+    }
 }
 
 // The entire purpose of this struct is to unubscribe this component when it is unmounted.
@@ -29,7 +102,7 @@ pub fn use_route(cx: &ScopeState) -> &ParsedRoute {
 // Instead, we hide the drop implementation on this private type exclusive to the hook,
 // and reveal our cached version of UseRoute to the component.
 struct UseRouteListener {
-    route: Arc<ParsedRoute>,
+    state: UseRoute,
     router: Arc<RouterCore>,
     scope: ScopeId,
 }

+ 0 - 6
packages/router/src/lib.rs

@@ -2,12 +2,8 @@
 #![warn(missing_docs)]
 
 mod hooks {
-    mod use_param;
-    mod use_query;
     mod use_route;
     mod use_router;
-    pub use use_param::*;
-    pub use use_query::*;
     pub use use_route::*;
     pub use use_router::*;
 }
@@ -29,10 +25,8 @@ mod components {
 pub use components::*;
 
 mod cfg;
-mod location;
 mod routecontext;
 mod service;
-mod utils;
 
 pub use routecontext::*;
 pub use service::*;

+ 0 - 66
packages/router/src/location.rs

@@ -1,66 +0,0 @@
-use std::str::FromStr;
-
-use url::Url;
-
-pub struct ParsedRoute {
-    pub(crate) url: url::Url,
-}
-
-impl ParsedRoute {
-    pub(crate) fn new(url: Url) -> Self {
-        Self { url }
-    }
-
-    // get the underlying url
-    pub fn url(&self) -> &Url {
-        &self.url
-    }
-
-    pub fn query(&self) -> Option<&String> {
-        None
-    }
-
-    /// Returns the nth segment in the path. Paths that end with a slash have
-    /// the slash removed before determining the segments. If the path has
-    /// fewer segments than `n` then this method returns `None`.
-    pub fn nth_segment(&self, n: usize) -> Option<&str> {
-        self.url.path_segments()?.nth(n)
-    }
-
-    /// Returns the last segment in the path. Paths that end with a slash have
-    /// the slash removed before determining the segments. The root path, `/`,
-    /// will return an empty string.
-    pub fn last_segment(&self) -> Option<&str> {
-        self.url.path_segments()?.last()
-    }
-
-    /// Get the named parameter from the path, as defined in your router. The
-    /// value will be parsed into the type specified by `T` by calling
-    /// `value.parse::<T>()`. This method returns `None` if the named
-    /// parameter does not exist in the current path.
-    pub fn segment<T>(&self, name: &str) -> Option<&str>
-    where
-        T: FromStr,
-    {
-        self.url.path_segments()?.find(|&f| f.eq(name))
-    }
-
-    /// Get the named parameter from the path, as defined in your router. The
-    /// value will be parsed into the type specified by `T` by calling
-    /// `value.parse::<T>()`. This method returns `None` if the named
-    /// parameter does not exist in the current path.
-    pub fn parse_segment<T>(&self, name: &str) -> Option<Result<T, T::Err>>
-    where
-        T: FromStr,
-    {
-        self.url
-            .path_segments()?
-            .find(|&f| f.eq(name))
-            .map(|f| f.parse::<T>())
-    }
-}
-
-#[test]
-fn parses_location() {
-    let route = ParsedRoute::new(Url::parse("app:///foo/bar?baz=qux&quux=corge").unwrap());
-}

+ 157 - 136
packages/router/src/service.rs

@@ -1,7 +1,7 @@
 // todo: how does router work in multi-window contexts?
 // does each window have its own router? probably, lol
 
-use crate::{cfg::RouterCfg, location::ParsedRoute};
+use crate::cfg::RouterCfg;
 use dioxus_core::ScopeId;
 use futures_channel::mpsc::UnboundedSender;
 use std::any::Any;
@@ -41,135 +41,103 @@ use url::Url;
 /// - On desktop, mobile, and SSR, this is just a Vec of Strings. Currently on
 ///   desktop, there is no way to tap into forward/back for the app unless explicitly set.
 pub struct RouterCore {
-    pub root_found: Cell<Option<ScopeId>>,
+    pub(crate) route_found: Cell<Option<ScopeId>>,
 
-    pub stack: RefCell<Vec<Arc<ParsedRoute>>>,
+    pub(crate) stack: RefCell<Vec<Arc<ParsedRoute>>>,
 
-    pub router_needs_update: Cell<bool>,
+    pub(crate) tx: UnboundedSender<RouteEvent>,
 
-    pub tx: UnboundedSender<RouteEvent>,
+    pub(crate) slots: Rc<RefCell<HashMap<ScopeId, String>>>,
 
-    pub slots: Rc<RefCell<HashMap<ScopeId, String>>>,
+    pub(crate) onchange_listeners: Rc<RefCell<HashSet<ScopeId>>>,
 
-    pub onchange_listeners: Rc<RefCell<HashSet<ScopeId>>>,
+    pub(crate) history: Box<dyn RouterProvider>,
 
-    pub query_listeners: Rc<RefCell<HashMap<ScopeId, String>>>,
+    pub(crate) cfg: RouterCfg,
+}
 
-    pub semgment_listeners: Rc<RefCell<HashMap<ScopeId, String>>>,
+/// A shared type for the RouterCore.
+pub type RouterService = Arc<RouterCore>;
 
-    pub history: Box<dyn RouterProvider>,
+/// A route is a combination of window title, saved state, and a URL.
+#[derive(Debug, Clone)]
+pub struct ParsedRoute {
+    /// The URL of the route.
+    pub url: Url,
 
-    pub cfg: RouterCfg,
-}
+    /// The title of the route.
+    pub title: Option<String>,
 
-pub type RouterService = Arc<RouterCore>;
+    /// The serialized state of the route.
+    pub serialized_state: Option<String>,
+}
 
 #[derive(Debug)]
-pub enum RouteEvent {
-    Push(String),
+pub(crate) enum RouteEvent {
+    Push {
+        route: String,
+        title: Option<String>,
+        serialized_state: Option<String>,
+    },
+    Replace {
+        route: String,
+        title: Option<String>,
+        serialized_state: Option<String>,
+    },
     Pop,
 }
 
 impl RouterCore {
-    pub fn new(tx: UnboundedSender<RouteEvent>, cfg: RouterCfg) -> Arc<Self> {
+    pub(crate) fn new(tx: UnboundedSender<RouteEvent>, cfg: RouterCfg) -> Arc<Self> {
         #[cfg(feature = "web")]
         let history = Box::new(web::new(tx.clone()));
 
         #[cfg(not(feature = "web"))]
-        let history = Box::new(hash::create_router());
+        let history = Box::new(hash::new());
 
-        let route = Arc::new(ParsedRoute::new(history.init_location()));
+        let route = Arc::new(history.init_location());
 
         Arc::new(Self {
             cfg,
             tx,
-            root_found: Cell::new(None),
+            route_found: Cell::new(None),
             stack: RefCell::new(vec![route]),
             slots: Default::default(),
-            semgment_listeners: Default::default(),
-            query_listeners: Default::default(),
             onchange_listeners: Default::default(),
             history,
-            router_needs_update: Default::default(),
         })
     }
 
-    pub fn handle_route_event(&self, msg: RouteEvent) -> Option<Arc<ParsedRoute>> {
-        log::debug!("handling route event {:?}", msg);
-        self.root_found.set(None);
-
-        match msg {
-            RouteEvent::Push(route) => {
-                let cur = self.current_location();
-
-                let new_url = cur.url.join(&route).ok().unwrap();
-
-                self.history.push(new_url.as_str());
-
-                let route = Arc::new(ParsedRoute::new(new_url));
-
-                self.stack.borrow_mut().push(route.clone());
-
-                Some(route)
-            }
-            RouteEvent::Pop => {
-                let mut stack = self.stack.borrow_mut();
-                if stack.len() == 1 {
-                    return None;
-                }
-
-                self.history.pop();
-                stack.pop()
-            }
-        }
-    }
-
     /// Push a new route to the history.
     ///
     /// This will trigger a route change event.
     ///
     /// This does not modify the current route
-    pub fn push_route(&self, route: &str) {
-        // convert the users route to our internal format
-        self.tx
-            .unbounded_send(RouteEvent::Push(route.to_string()))
-            .unwrap();
+    pub fn push_route(&self, route: &str, title: Option<String>, serialized_state: Option<String>) {
+        let _ = self.tx.unbounded_send(RouteEvent::Push {
+            route: route.to_string(),
+            title,
+            serialized_state,
+        });
     }
 
     /// Pop the current route from the history.
-    ///
-    ///
     pub fn pop_route(&self) {
-        self.tx.unbounded_send(RouteEvent::Pop).unwrap();
-    }
-
-    pub(crate) fn register_total_route(&self, route: String, scope: ScopeId) {
-        let clean = clean_route(route);
-        self.slots.borrow_mut().insert(scope, clean);
+        let _ = self.tx.unbounded_send(RouteEvent::Pop);
     }
 
-    pub(crate) fn should_render(&self, scope: ScopeId) -> bool {
-        log::debug!("Checking render: {:?}", scope);
-
-        if let Some(root_id) = self.root_found.get() {
-            return root_id == scope;
-        }
-
-        let roots = self.slots.borrow();
-
-        if let Some(route) = roots.get(&scope) {
-            log::debug!("Registration found for scope {:?} {:?}", scope, route);
-
-            if route_matches_path(&self.current_location(), route) || route.is_empty() {
-                self.root_found.set(Some(scope));
-                true
-            } else {
-                false
-            }
-        } else {
-            log::debug!("no route found for scope: {:?}", scope);
-            false
-        }
+    /// Instead of pushing a new route, replaces the current route.
+    pub fn replace_route(
+        &self,
+        route: &str,
+        title: Option<String>,
+        serialized_state: Option<String>,
+    ) {
+        let _ = self.tx.unbounded_send(RouteEvent::Replace {
+            route: route.to_string(),
+            title,
+            serialized_state,
+        });
     }
 
     /// Get the current location of the Router
@@ -177,12 +145,7 @@ impl RouterCore {
         self.stack.borrow().last().unwrap().clone()
     }
 
-    pub fn query_current_location(&self) -> HashMap<String, String> {
-        todo!()
-        // self.history.borrow().query()
-    }
-
-    /// Get the current location of the Router
+    /// Get the current native location of the Router
     pub fn native_location<T: 'static>(&self) -> Option<Box<T>> {
         self.history.native_location().downcast::<T>().ok()
     }
@@ -200,6 +163,35 @@ impl RouterCore {
     pub fn unsubscribe_onchange(&self, id: ScopeId) {
         self.onchange_listeners.borrow_mut().remove(&id);
     }
+
+    pub(crate) fn register_total_route(&self, route: String, scope: ScopeId) {
+        let clean = clean_route(route);
+        self.slots.borrow_mut().insert(scope, clean);
+    }
+
+    pub(crate) fn should_render(&self, scope: ScopeId) -> bool {
+        if let Some(root_id) = self.route_found.get() {
+            return root_id == scope;
+        }
+
+        let roots = self.slots.borrow();
+
+        if let Some(route) = roots.get(&scope) {
+            if route_matches_path(
+                &self.current_location().url,
+                route,
+                self.cfg.base_url.as_ref(),
+            ) || route.is_empty()
+            {
+                self.route_found.set(Some(scope));
+                true
+            } else {
+                false
+            }
+        } else {
+            false
+        }
+    }
 }
 
 fn clean_route(route: String) -> String {
@@ -222,27 +214,26 @@ fn clean_path(path: &str) -> &str {
     }
 }
 
-fn route_matches_path(cur: &ParsedRoute, attempt: &str) -> bool {
-    let cur_pieces = cur.url.path_segments().unwrap().collect::<Vec<_>>();
+fn route_matches_path(cur: &Url, attempt: &str, base_url: Option<&String>) -> bool {
+    let cur_piece_iter = cur.path_segments().unwrap();
+
+    let cur_pieces = match base_url {
+        // baseurl is naive right now and doesn't support multiple nesting levels
+        Some(_) => cur_piece_iter.skip(1).collect::<Vec<_>>(),
+        None => cur_piece_iter.collect::<Vec<_>>(),
+    };
+
     let attempt_pieces = clean_path(attempt).split('/').collect::<Vec<_>>();
 
     if attempt == "/" && cur_pieces.len() == 1 && cur_pieces[0].is_empty() {
         return true;
     }
 
-    log::debug!(
-        "Comparing cur {:?} to attempt {:?}",
-        cur_pieces,
-        attempt_pieces
-    );
-
     if attempt_pieces.len() != cur_pieces.len() {
         return false;
     }
 
     for (i, r) in attempt_pieces.iter().enumerate() {
-        log::debug!("checking route: {:?}", r);
-
         // If this is a parameter then it matches as long as there's
         // _any_thing in that spot in the path.
         if r.starts_with(':') {
@@ -257,30 +248,37 @@ fn route_matches_path(cur: &ParsedRoute, attempt: &str) -> bool {
     true
 }
 
-pub trait RouterProvider {
-    fn push(&self, path: &str);
-    fn pop(&self);
+pub(crate) trait RouterProvider {
+    fn push(&self, route: &ParsedRoute);
+    fn replace(&self, route: &ParsedRoute);
     fn native_location(&self) -> Box<dyn Any>;
-    fn init_location(&self) -> Url;
+    fn init_location(&self) -> ParsedRoute;
 }
 
+#[cfg(not(feature = "web"))]
 mod hash {
     use super::*;
 
+    pub fn new() -> HashRouter {
+        HashRouter {}
+    }
+
     /// a simple cross-platform hash-based router
     pub struct HashRouter {}
 
     impl RouterProvider for HashRouter {
-        fn push(&self, _path: &str) {}
+        fn push(&self, _route: &ParsedRoute) {}
 
         fn native_location(&self) -> Box<dyn Any> {
             Box::new(())
         }
 
-        fn pop(&self) {}
-
-        fn init_location(&self) -> Url {
-            Url::parse("app:///").unwrap()
+        fn init_location(&self) -> ParsedRoute {
+            ParsedRoute {
+                url: Url::parse("app:///").unwrap(),
+                title: None,
+                serialized_state: None,
+            }
         }
     }
 }
@@ -288,50 +286,73 @@ mod hash {
 #[cfg(feature = "web")]
 mod web {
     use super::RouterProvider;
-    use crate::RouteEvent;
+    use crate::{ParsedRoute, RouteEvent};
 
     use futures_channel::mpsc::UnboundedSender;
-    use gloo::{
-        events::EventListener,
-        history::{BrowserHistory, History},
-    };
+    use gloo_events::EventListener;
     use std::any::Any;
-    use url::Url;
+    use web_sys::History;
 
     pub struct WebRouter {
         // keep it around so it drops when the router is dropped
-        _listener: gloo::events::EventListener,
+        _listener: gloo_events::EventListener,
 
-        history: BrowserHistory,
+        window: web_sys::Window,
+        history: History,
     }
 
     impl RouterProvider for WebRouter {
-        fn push(&self, path: &str) {
-            self.history.push(path);
-            // use gloo::history;
-            // web_sys::window()
-            //     .unwrap()
-            //     .location()
-            //     .set_href(path)
-            //     .unwrap();
+        fn push(&self, route: &ParsedRoute) {
+            let ParsedRoute {
+                url,
+                title,
+                serialized_state,
+            } = route;
+
+            let _ = self.history.push_state_with_url(
+                &wasm_bindgen::JsValue::from_str(serialized_state.as_deref().unwrap_or("")),
+                title.as_deref().unwrap_or(""),
+                Some(url.as_str()),
+            );
         }
 
-        fn native_location(&self) -> Box<dyn Any> {
-            todo!()
+        fn replace(&self, route: &ParsedRoute) {
+            let ParsedRoute {
+                url,
+                title,
+                serialized_state,
+            } = route;
+
+            let _ = self.history.replace_state_with_url(
+                &wasm_bindgen::JsValue::from_str(serialized_state.as_deref().unwrap_or("")),
+                title.as_deref().unwrap_or(""),
+                Some(url.as_str()),
+            );
         }
 
-        fn pop(&self) {
-            // set the title, maybe?
+        fn native_location(&self) -> Box<dyn Any> {
+            Box::new(self.window.location())
         }
 
-        fn init_location(&self) -> Url {
-            url::Url::parse(&web_sys::window().unwrap().location().href().unwrap()).unwrap()
+        fn init_location(&self) -> ParsedRoute {
+            ParsedRoute {
+                url: url::Url::parse(&web_sys::window().unwrap().location().href().unwrap())
+                    .unwrap(),
+                title: web_sys::window()
+                    .unwrap()
+                    .document()
+                    .unwrap()
+                    .title()
+                    .into(),
+                serialized_state: None,
+            }
         }
     }
 
-    pub fn new(tx: UnboundedSender<RouteEvent>) -> WebRouter {
+    pub(crate) fn new(tx: UnboundedSender<RouteEvent>) -> WebRouter {
         WebRouter {
-            history: BrowserHistory::new(),
+            history: web_sys::window().unwrap().history().unwrap(),
+            window: web_sys::window().unwrap(),
             _listener: EventListener::new(&web_sys::window().unwrap(), "popstate", move |_| {
                 let _ = tx.unbounded_send(RouteEvent::Pop);
             }),

+ 0 - 6
packages/router/src/utils.rs

@@ -1,6 +0,0 @@
-// use wasm_bindgen::JsCast;
-// use web_sys::window;
-
-pub(crate) fn strip_slash_suffix(path: &str) -> &str {
-    path.strip_suffix('/').unwrap_or(path)
-}