From f2979cb12ad6ba33e3cbbd8c425a35e54eeab0f8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Mar 2022 17:57:57 -0500 Subject: [PATCH 01/14] feat: apply local router changes --- packages/core/src/scopes.rs | 25 +- packages/desktop/src/desktop_context.rs | 12 +- packages/fermi/src/hooks/atom_root.rs | 4 +- packages/fermi/src/hooks/init_atom_root.rs | 4 +- packages/hooks/src/use_shared_state.rs | 8 +- packages/hooks/src/usecoroutine.rs | 12 +- packages/router/Cargo.toml | 31 +- packages/router/examples/simple.rs | 20 +- packages/router/src/cfg.rs | 4 + packages/router/src/components/link.rs | 145 ++++--- packages/router/src/components/redirect.rs | 8 + packages/router/src/components/route.rs | 54 ++- packages/router/src/components/router.rs | 85 +++- .../src/{platform/desktop.rs => error.rs} | 0 packages/router/src/hooks/use_param.rs | 5 + .../mobile.rs => hooks/use_query.rs} | 0 packages/router/src/hooks/use_route.rs | 101 +---- packages/router/src/hooks/use_router.rs | 10 + packages/router/src/lib.rs | 37 +- packages/router/src/location.rs | 66 ++++ packages/router/src/platform/mod.rs | 4 - packages/router/src/platform/web.rs | 125 ------ packages/router/src/routecontext.rs | 21 +- packages/router/src/service.rs | 367 ++++++++++++------ packages/router/tests/desktop_router.rs | 0 packages/router/tests/ssr_router.rs | 29 ++ .../router/tests/{route.rs => web_router.rs} | 0 tests/sharedstate.rs | 11 +- 28 files changed, 672 insertions(+), 516 deletions(-) create mode 100644 packages/router/src/cfg.rs create mode 100644 packages/router/src/components/redirect.rs rename packages/router/src/{platform/desktop.rs => error.rs} (100%) create mode 100644 packages/router/src/hooks/use_param.rs rename packages/router/src/{platform/mobile.rs => hooks/use_query.rs} (100%) create mode 100644 packages/router/src/hooks/use_router.rs create mode 100644 packages/router/src/location.rs delete mode 100644 packages/router/src/platform/mod.rs delete mode 100644 packages/router/src/platform/web.rs create mode 100644 packages/router/tests/desktop_router.rs create mode 100644 packages/router/tests/ssr_router.rs rename packages/router/tests/{route.rs => web_router.rs} (100%) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 7e415801b..ce1982e4f 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -327,12 +327,12 @@ impl ScopeArena { while let Some(id) = cur_el.take() { if let Some(el) = nodes.get(id.0) { let real_el = unsafe { &**el }; - log::debug!("looking for listener on {:?}", real_el); + log::trace!("looking for listener on {:?}", real_el); if let VNode::Element(real_el) = real_el { for listener in real_el.listeners.borrow().iter() { if listener.event == event.name { - log::debug!("calling listener {:?}", listener.event); + log::trace!("calling listener {:?}", listener.event); if state.canceled.get() { // stop bubbling if canceled break; @@ -489,7 +489,7 @@ pub struct ScopeState { pub(crate) hook_idx: Cell, // shared state -> todo: move this out of scopestate - pub(crate) shared_contexts: RefCell>>, + pub(crate) shared_contexts: RefCell>>, pub(crate) tasks: Rc, } @@ -676,11 +676,10 @@ impl ScopeState { /// rsx!(cx, div { "hello {state.0}" }) /// } /// ``` - pub fn provide_context(&self, value: T) -> Rc { - let value = Rc::new(value); + pub fn provide_context(&self, value: T) -> T { self.shared_contexts .borrow_mut() - .insert(TypeId::of::(), value.clone()) + .insert(TypeId::of::(), Box::new(value.clone())) .and_then(|f| f.downcast::().ok()); value } @@ -703,14 +702,12 @@ impl ScopeState { /// rsx!(cx, div { "hello {state.0}" }) /// } /// ``` - pub fn provide_root_context(&self, value: T) -> Rc { - let value = Rc::new(value); - + pub fn provide_root_context(&self, value: T) -> T { // if we *are* the root component, then we can just provide the context directly if self.scope_id() == ScopeId(0) { self.shared_contexts .borrow_mut() - .insert(TypeId::of::(), value.clone()) + .insert(TypeId::of::(), Box::new(value.clone())) .and_then(|f| f.downcast::().ok()); return value; } @@ -724,7 +721,7 @@ impl ScopeState { let exists = parent .shared_contexts .borrow_mut() - .insert(TypeId::of::(), value.clone()); + .insert(TypeId::of::(), Box::new(value.clone())); if exists.is_some() { log::warn!("Context already provided to parent scope - replacing it"); @@ -739,9 +736,9 @@ impl ScopeState { } /// Try to retrieve a SharedState with type T from the any parent Scope. - pub fn consume_context(&self) -> Option> { + pub fn consume_context(&self) -> Option { if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) { - Some(shared.clone().downcast::().unwrap()) + Some((*shared.downcast_ref::().unwrap()).clone()) } else { let mut search_parent = self.parent_scope; @@ -749,7 +746,7 @@ impl ScopeState { // safety: all parent pointers are valid thanks to the bump arena let parent = unsafe { &*parent_ptr }; if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { - return Some(shared.clone().downcast::().unwrap()); + return Some(shared.downcast_ref::().unwrap().clone()); } search_parent = parent.parent_scope; } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index eaff13750..93162842b 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,14 +1,15 @@ -use std::rc::Rc; - +use crate::controller::DesktopController; use dioxus_core::ScopeState; +use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; +use wry::application::window::Fullscreen as WryFullscreen; use UserWindowEvent::*; pub type ProxyType = EventLoopProxy; /// Get an imperative handle to the current window -pub fn use_window(cx: &ScopeState) -> &Rc { +pub fn use_window(cx: &ScopeState) -> &DesktopContext { cx.use_hook(|_| cx.consume_context::()) .as_ref() .unwrap() @@ -120,11 +121,6 @@ impl DesktopContext { } } -use wry::application::event_loop::ControlFlow; -use wry::application::window::Fullscreen as WryFullscreen; - -use crate::controller::DesktopController; - #[derive(Debug)] pub enum UserWindowEvent { Update, diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs index d2945025b..72862a59f 100644 --- a/packages/fermi/src/hooks/atom_root.rs +++ b/packages/fermi/src/hooks/atom_root.rs @@ -4,8 +4,8 @@ use std::rc::Rc; // Returns the atom root, initiaizing it at the root of the app if it does not exist. pub fn use_atom_root(cx: &ScopeState) -> &Rc { - cx.use_hook(|_| match cx.consume_context::() { + cx.use_hook(|_| match cx.consume_context::>() { Some(root) => root, - None => cx.provide_root_context(AtomRoot::new(cx.schedule_update_any())), + None => cx.provide_root_context(Rc::new(AtomRoot::new(cx.schedule_update_any()))), }) } diff --git a/packages/fermi/src/hooks/init_atom_root.rs b/packages/fermi/src/hooks/init_atom_root.rs index edb8aafb8..12fa3efca 100644 --- a/packages/fermi/src/hooks/init_atom_root.rs +++ b/packages/fermi/src/hooks/init_atom_root.rs @@ -4,8 +4,8 @@ use std::rc::Rc; // Initializes the atom root and retuns it; pub fn use_init_atom_root(cx: &ScopeState) -> &Rc { - cx.use_hook(|_| match cx.consume_context::() { + cx.use_hook(|_| match cx.consume_context::>() { Some(ctx) => ctx, - None => cx.provide_context(AtomRoot::new(cx.schedule_update_any())), + None => cx.provide_context(Rc::new(AtomRoot::new(cx.schedule_update_any()))), }) } diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 3672302e9..78ac671c1 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -6,7 +6,7 @@ use std::{ sync::Arc, }; -type ProvidedState = RefCell>; +type ProvidedState = Rc>>; // Tracks all the subscribers to a shared State pub struct ProvidedStateInner { @@ -91,7 +91,7 @@ pub fn use_context<'a, T: 'static>(cx: &'a ScopeState) -> Option { - root: Option>>, + root: Option>, value: Option>>, scope_id: ScopeId, needs_notification: Cell, @@ -174,11 +174,11 @@ where /// pub fn use_context_provider(cx: &ScopeState, f: impl FnOnce() -> T) { cx.use_hook(|_| { - let state: ProvidedState = RefCell::new(ProvidedStateInner { + let state: ProvidedState = Rc::new(RefCell::new(ProvidedStateInner { value: Rc::new(RefCell::new(f())), notify_any: cx.schedule_update_any(), consumers: HashSet::new(), - }); + })); cx.provide_context(state) }); } diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index b4b2907ff..65a3ff52d 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -1,7 +1,6 @@ use dioxus_core::{ScopeState, TaskId}; pub use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use std::future::Future; -use std::rc::Rc; /// Maintain a handle over a future that can be paused, resumed, and canceled. /// @@ -76,7 +75,7 @@ where /// Get a handle to a coroutine higher in the tree /// /// See the docs for [`use_coroutine`] for more details. -pub fn use_coroutine_handle(cx: &ScopeState) -> Option<&Rc>> { +pub fn use_coroutine_handle(cx: &ScopeState) -> Option<&CoroutineHandle> { cx.use_hook(|_| cx.consume_context::>()) .as_ref() } @@ -86,6 +85,15 @@ pub struct CoroutineHandle { task: TaskId, } +impl Clone for CoroutineHandle { + fn clone(&self) -> Self { + Self { + tx: self.tx.clone(), + task: self.task, + } + } +} + impl CoroutineHandle { /// Get the ID of this coroutine #[must_use] diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 221372b50..6065fdddd 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -14,10 +14,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } - -serde = "1" -serde_urlencoded = "0.7" -# url = "2.2.2" +# serde = "1" +# serde_urlencoded = "0.7" +futures-channel = "0.3.21" +url = "2.2.2" # for wasm web-sys = { version = "0.3", features = [ @@ -33,17 +33,16 @@ 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.5", optional = true } +gloo = { version = "0.6", optional = true } log = "0.4.14" +thiserror = "1.0.30" +futures-util = "0.3.21" [features] -default = ["derive", "web"] +default = ["web"] web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] -desktop = [] -mobile = [] -derive = [] -wasm_test = [] +hash = [] [dev-dependencies] console_error_panic_hook = "0.1.7" @@ -52,9 +51,15 @@ log = "0.4.14" wasm-logger = "0.2.0" wasm-bindgen-test = "0.3" gloo-utils = "0.1.2" +dioxus-ssr = { path = "../ssr"} +dioxus-router = { path = ".", default-features = false } + +# [target.wasm32-unknown-unknown.dev-dependencies] +# dioxus-router = { path = ".", features = ["web"] } + + + [dev-dependencies.web-sys] version = "0.3" -features = [ - "Document", -] +features = ["Document"] diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple.rs index 4afb122ae..6c559494d 100644 --- a/packages/router/examples/simple.rs +++ b/packages/router/examples/simple.rs @@ -8,21 +8,25 @@ use dioxus_router::*; fn main() { console_error_panic_hook::set_once(); wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); - dioxus_web::launch(APP); + dioxus_web::launch(app); } -static APP: Component = |cx| { +fn app(cx: Scope) -> Element { cx.render(rsx! { Router { - onchange: move |route| log::info!("route changed to {}", route), - Route { to: "/", Home {} } - Route { to: "blog" - Route { to: "/", BlogList {} } - Route { to: ":id", BlogPost {} } + h1 { "Your app here" } + ul { + Link { to: "/", li { "home" }} + Link { to: "/blog", li { "blog" }} + Link { to: "/blog/tim", li { "tims' blog" }} + Link { to: "/blog/bill", li { "bills' blog" }} } + Route { to: "/", Home {} } + Route { to: "/blog/", BlogList {} } + Route { to: "/blog/:id/", BlogPost {} } } }) -}; +} fn Home(cx: Scope) -> Element { cx.render(rsx! { diff --git a/packages/router/src/cfg.rs b/packages/router/src/cfg.rs new file mode 100644 index 000000000..cec676ef2 --- /dev/null +++ b/packages/router/src/cfg.rs @@ -0,0 +1,4 @@ +#[derive(Default)] +pub struct RouterCfg { + pub base_url: Option, +} diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index b2b69108f..3699e0a2a 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -1,97 +1,116 @@ -use crate::RouterService; -use dioxus::Attribute; +use std::sync::Arc; + +use crate::RouterCore; use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_core_macro::{format_args_f, rsx, Props}; use dioxus_html as dioxus_elements; +/// Props for the [`Link`](struct.Link.html) component. #[derive(Props)] pub struct LinkProps<'a> { - to: &'a str, - - /// The url that gets pushed to the history stack + /// The route to link to. This can be a relative path, or a full URL. /// - /// You can either put in your own inline method or just autoderive the route using `derive(Routable)` - /// - /// ```rust, ignore - /// - /// Link { to: Route::Home, href: |_| "home".to_string() } - /// - /// // or - /// - /// Link { to: Route::Home, href: Route::as_url } + /// ```rust + /// // Absolute path + /// Link { to: "/home", "Go Home" } /// + /// // Relative path + /// Link { to: "../", "Go Up" } /// ``` - #[props(default, strip_option)] - href: Option<&'a str>, + pub to: &'a str, + /// Set the class of the inner link ['a'](https://www.w3schools.com/tags/tag_a.asp) element. + /// + /// This can be useful when styling the inner link element. #[props(default, strip_option)] - class: Option<&'a str>, + pub class: Option<&'a str>, + /// Set the ID of the inner link ['a'](https://www.w3schools.com/tags/tag_a.asp) element. + /// + /// This can be useful when styling the inner link element. #[props(default, strip_option)] - id: Option<&'a str>, + pub id: Option<&'a str>, + /// Set the title of the window after the link is clicked.. #[props(default, strip_option)] - title: Option<&'a str>, + pub title: Option<&'a str>, + /// Autodetect if a link is external or not. + /// + /// This is automatically set to `true` and will use http/https detection #[props(default = true)] - autodetect: bool, + pub autodetect: bool, /// Is this link an external link? #[props(default = false)] - external: bool, + pub external: bool, /// New tab? #[props(default = false)] - new_tab: bool, + pub new_tab: bool, - children: Element<'a>, - - #[props(default)] - attributes: Option<&'a [Attribute<'a>]>, + /// Pass children into the `` element + pub children: Element<'a>, } +/// A component that renders a link to a route. +/// +/// `Link` components are just [``](https://www.w3schools.com/tags/tag_a.asp) elements +/// that link to different pages *within* your single-page app. +/// +/// If you need to link to a resource outside of your app, then just use a regular +/// `` element directly. +/// +/// # Examples +/// +/// ```rust +/// fn Header(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// Link { to: "/home", "Go Home" } +/// }) +/// } +/// ``` pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - if let Some(service) = cx.consume_context::() { - let LinkProps { - to, - href, - class, - id, - title, - autodetect, - external, - new_tab, - children, - .. - } = cx.props; + let svc = cx.use_hook(|_| cx.consume_context::>()); - let is_http = to.starts_with("http") || to.starts_with("https"); - let outerlink = (*autodetect && is_http) || *external; + let LinkProps { + to, + class, + id, + title, + autodetect, + external, + new_tab, + children, + .. + } = cx.props; - let prevent_default = if outerlink { "" } else { "onclick" }; + let is_http = to.starts_with("http") || to.starts_with("https"); + let outerlink = (*autodetect && is_http) || *external; + let prevent_default = if outerlink { "" } else { "onclick" }; - return cx.render(rsx! { - a { - href: "{to}", - class: format_args!("{}", class.unwrap_or("")), - id: format_args!("{}", id.unwrap_or("")), - title: format_args!("{}", title.unwrap_or("")), - prevent_default: "{prevent_default}", - target: format_args!("{}", if *new_tab { "_blank" } else { "" }), - onclick: move |_| { - if !outerlink { + cx.render(rsx! { + a { + href: "{to}", + class: format_args!("{}", class.unwrap_or("")), + id: format_args!("{}", id.unwrap_or("")), + title: format_args!("{}", title.unwrap_or("")), + prevent_default: "{prevent_default}", + target: format_args!("{}", if *new_tab { "_blank" } else { "" }), + onclick: move |_| { + if !outerlink { + if let Some(service) = svc { service.push_route(to); + } else { + log::error!( + "Attempted to create a Link to {} outside of a Router context", + cx.props.to, + ); } - }, - - &cx.props.children - } - }); - } - log::warn!( - "Attempted to create a Link to {} outside of a Router context", - cx.props.to, - ); - None + } + }, + children + } + }) } diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs new file mode 100644 index 000000000..f9b409e25 --- /dev/null +++ b/packages/router/src/components/redirect.rs @@ -0,0 +1,8 @@ +use dioxus_core::prelude::*; + + +// The entire point of this component is to immediately redirect to the given path. +pub fn Redirect(cx: Scope) -> Element { + // + None +} diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index e4dd2f9c8..00fa28af2 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -1,4 +1,4 @@ -use dioxus_core::Element; +use std::sync::Arc; use dioxus_core as dioxus; use dioxus_core::prelude::*; @@ -6,31 +6,42 @@ use dioxus_core_macro::Props; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; -use crate::{RouteContext, RouterService}; +use crate::{RouteContext, RouterCore}; +/// Props for the [`Route`](struct.Route.html) component. #[derive(Props)] pub struct RouteProps<'a> { - to: &'a str, + /// The path to match. + pub to: &'a str, - children: Element<'a>, - - #[props(default)] - fallback: bool, + /// The component to render when the path matches. + pub children: Element<'a>, } +/// A component that conditionally renders children based on the current location. +/// +/// # Example +/// +///```rust +/// rsx!( +/// Router { +/// Route { to: "/home", Home {} } +/// Route { to: "/about", About {} } +/// Route { to: "/Blog", Blog {} } +/// } +/// ) +/// ``` pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { - // now we want to submit let router_root = cx - .use_hook(|_| cx.consume_context::()) + .use_hook(|_| cx.consume_context::>()) .as_ref()?; cx.use_hook(|_| { // create a bigger, better, longer route if one above us exists let total_route = match cx.consume_context::() { - Some(ctx) => ctx.total_route.to_string(), + Some(ctx) => ctx.total_route, None => cx.props.to.to_string(), }; - // log::trace!("total route for {} is {}", cx.props.to, total_route); // provide our route context let route_context = cx.provide_context(RouteContext { @@ -39,28 +50,15 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { }); // submit our rout - router_root.register_total_route( - route_context.total_route.clone(), - cx.scope_id(), - cx.props.fallback, - ); - - Some(RouteInner {}) + router_root.register_total_route(route_context.total_route, cx.scope_id()); }); - // log::trace!("Checking route {}", cx.props.to); - + log::debug!("Checking Route: {:?}", cx.props.to); if router_root.should_render(cx.scope_id()) { + log::debug!("Route should render: {:?}", cx.scope_id()); cx.render(rsx!(&cx.props.children)) } else { + log::debug!("Route should *not* render: {:?}", cx.scope_id()); None } } - -struct RouteInner {} - -impl Drop for RouteInner { - fn drop(&mut self) { - // todo!() - } -} diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index c117c6650..e3cbec0fd 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -1,35 +1,90 @@ -use dioxus_core::Element; +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 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 crate::RouterService; - +/// The props for the [`Router`](fn.Router.html) component. #[derive(Props)] pub struct RouterProps<'a> { - children: Element<'a>, + /// The routes and elements that should be rendered when the path matches. + /// + /// If elements are not contained within Routes, the will be rendered + /// regardless of the path. + pub children: Element<'a>, + /// The URL to point at + /// + /// This will be used to trim any latent segments from the URL when your app is + /// not deployed to the root of the domain. + #[props(optional)] + pub base_url: Option<&'a str>, + + /// Hook into the router when the route is changed. + /// + /// This lets you easily implement redirects #[props(default)] - onchange: EventHandler<'a, String>, + pub onchange: EventHandler<'a, Arc>, } +/// A component that conditionally renders children based on the current location of the app. +/// +/// Uses BrowserRouter in the browser and HashRouter everywhere else. +/// +/// 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 { - log::debug!("running router {:?}", cx.scope_id()); + let call_onchange = cx.use_hook(|_| Rc::new(Cell::new(false))); + let svc = cx.use_hook(|_| { - let update = cx.schedule_update_any(); - cx.provide_context(RouterService::new(update, cx.scope_id())) + let (tx, mut rx) = futures_channel::mpsc::unbounded::(); + + let base_url = cx.props.base_url.map(|s| s.to_string()); + + let svc = RouterCore::new(tx, RouterCfg { base_url }); + + 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); + + for listener in svc.onchange_listeners.borrow().iter() { + regen_route(*listener); + } + + for route in svc.slots.borrow().keys() { + regen_route(*route); + } + } + } + } + }); + + cx.provide_context(svc) }); - let any_pending = svc.pending_events.borrow().len() > 0; - svc.pending_events.borrow_mut().clear(); - - if any_pending { - let location = svc.current_location(); - let path = location.path(); - cx.props.onchange.call(path.to_string()); + if call_onchange.get() { + cx.props.onchange.call(svc.clone()); + call_onchange.set(false); } cx.render(rsx!(&cx.props.children)) diff --git a/packages/router/src/platform/desktop.rs b/packages/router/src/error.rs similarity index 100% rename from packages/router/src/platform/desktop.rs rename to packages/router/src/error.rs diff --git a/packages/router/src/hooks/use_param.rs b/packages/router/src/hooks/use_param.rs new file mode 100644 index 000000000..da0aad89b --- /dev/null +++ b/packages/router/src/hooks/use_param.rs @@ -0,0 +1,5 @@ +use dioxus_core::prelude::*; + +pub fn use_param(cx: &ScopeState) -> Option { + todo!() +} diff --git a/packages/router/src/platform/mobile.rs b/packages/router/src/hooks/use_query.rs similarity index 100% rename from packages/router/src/platform/mobile.rs rename to packages/router/src/hooks/use_query.rs diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 4de6dff12..435d54920 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,94 +1,27 @@ use dioxus_core::{ScopeId, ScopeState}; -use gloo::history::{HistoryResult, Location}; -use serde::de::DeserializeOwned; -use std::{rc::Rc, str::FromStr}; +use std::{rc::Rc, str::FromStr, sync::Arc}; -use crate::RouterService; - -/// This struct provides is a wrapper around the internal router -/// implementation, with methods for getting information about the current -/// route. -#[derive(Clone)] -pub struct UseRoute { - router: Rc, -} - -impl UseRoute { - /// This method simply calls the [`Location::query`] method. - pub fn query(&self) -> HistoryResult - where - T: DeserializeOwned, - { - self.current_location().query::() - } - - /// 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 { - let mut segments = self.path_segments(); - let len = segments.len(); - if len - 1 < n { - return None; - } - Some(segments.remove(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) -> String { - let mut segments = self.path_segments(); - segments.remove(segments.len() - 1) - } - - /// 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::()`. This method returns `None` if the named - /// parameter does not exist in the current path. - pub fn segment(&self, name: &str) -> Option> - where - T: FromStr, - { - self.router - .current_path_params() - .get(name) - .and_then(|v| Some(v.parse::())) - } - - /// Returns the [Location] for the current route. - pub fn current_location(&self) -> Location { - self.router.current_location() - } - - fn path_segments(&self) -> Vec { - let location = self.router.current_location(); - let path = location.path(); - if path == "/" { - return vec![String::new()]; - } - let stripped = &location.path()[1..]; - stripped.split('/').map(str::to_string).collect::>() - } -} +use crate::{location::ParsedRoute, RouterCore, RouterService}; /// 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) -> &UseRoute { - &cx.use_hook(|_| { +pub fn use_route(cx: &ScopeState) -> &ParsedRoute { + let handle = cx.use_hook(|_| { let router = cx - .consume_context::() + .consume_context::>() .expect("Cannot call use_route outside the scope of a Router component"); router.subscribe_onchange(cx.scope_id()); UseRouteListener { - router: UseRoute { router }, + route: router.current_location(), + router, scope: cx.scope_id(), } - }) - .router + }); + + &handle.route } // The entire purpose of this struct is to unubscribe this component when it is unmounted. @@ -96,19 +29,13 @@ pub fn use_route(cx: &ScopeState) -> &UseRoute { // 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 { - router: UseRoute, + route: Arc, + router: Arc, scope: ScopeId, } + impl Drop for UseRouteListener { fn drop(&mut self) { - self.router.router.unsubscribe_onchange(self.scope) + self.router.unsubscribe_onchange(self.scope) } } - -/// This hook provides access to the `RouterService` for the app. -pub fn use_router(cx: &ScopeState) -> &Rc { - cx.use_hook(|_| { - cx.consume_context::() - .expect("Cannot call use_route outside the scope of a Router component") - }) -} diff --git a/packages/router/src/hooks/use_router.rs b/packages/router/src/hooks/use_router.rs new file mode 100644 index 000000000..5e8fb71b6 --- /dev/null +++ b/packages/router/src/hooks/use_router.rs @@ -0,0 +1,10 @@ +use crate::RouterService; +use dioxus_core::ScopeState; + +/// This hook provides access to the `RouterService` for the app. +pub fn use_router(cx: &ScopeState) -> &RouterService { + cx.use_hook(|_| { + cx.consume_context::() + .expect("Cannot call use_route outside the scope of a Router component") + }) +} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index 309cef905..78b310c7f 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -1,38 +1,35 @@ -#![allow(warnings)] -//! Dioxus-Router -//! -//! A simple match-based router and router service for most routing needs. -//! -//! Dioxus-Router is not a *declarative* router. Instead it uses a simple parse-match -//! pattern which can be derived via a macro. -//! -//! ```rust -//! fn app(cx: Scope) -> Element { -//! -//! } -//! ``` +#![doc = include_str!("../README.md")] +#![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::*; } pub use hooks::*; mod components { #![allow(non_snake_case)] - mod router; - pub use router::*; - - mod route; - pub use route::*; - mod link; + mod redirect; + mod route; + mod router; + pub use link::*; + pub use redirect::*; + pub use route::*; + pub use router::*; } pub use components::*; -mod platform; +mod cfg; +mod location; mod routecontext; mod service; mod utils; diff --git a/packages/router/src/location.rs b/packages/router/src/location.rs new file mode 100644 index 000000000..64cb66522 --- /dev/null +++ b/packages/router/src/location.rs @@ -0,0 +1,66 @@ +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::()`. This method returns `None` if the named + /// parameter does not exist in the current path. + pub fn segment(&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::()`. This method returns `None` if the named + /// parameter does not exist in the current path. + pub fn parse_segment(&self, name: &str) -> Option> + where + T: FromStr, + { + self.url + .path_segments()? + .find(|&f| f.eq(name)) + .map(|f| f.parse::()) + } +} + +#[test] +fn parses_location() { + let route = ParsedRoute::new(Url::parse("app:///foo/bar?baz=qux&quux=corge").unwrap()); +} diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs deleted file mode 100644 index ec59a4703..000000000 --- a/packages/router/src/platform/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub trait RouterProvider { - fn get_current_route(&self) -> String; - fn listen(&self, callback: Box); -} diff --git a/packages/router/src/platform/web.rs b/packages/router/src/platform/web.rs deleted file mode 100644 index 5b2673273..000000000 --- a/packages/router/src/platform/web.rs +++ /dev/null @@ -1,125 +0,0 @@ -use web_sys::{window, Event}; -pub struct RouterService { - historic_routes: Vec, - history_service: RefCell, - base_ur: RefCell>, -} - -impl RouterService { - fn push_route(&self, r: R) { - todo!() - // self.historic_routes.borrow_mut().push(r); - } - - fn get_current_route(&self) -> &str { - todo!() - } - - fn update_route_impl(&self, url: String, push: bool) { - let history = web_sys::window().unwrap().history().expect("no history"); - let base = self.base_ur.borrow(); - let path = match base.as_ref() { - Some(base) => { - let path = format!("{}{}", base, url); - if path.is_empty() { - "/".to_string() - } else { - path - } - } - None => url, - }; - - if push { - history - .push_state_with_url(&JsValue::NULL, "", Some(&path)) - .expect("push history"); - } else { - history - .replace_state_with_url(&JsValue::NULL, "", Some(&path)) - .expect("replace history"); - } - let event = Event::new("popstate").unwrap(); - - web_sys::window() - .unwrap() - .dispatch_event(&event) - .expect("dispatch"); - } -} - -pub fn use_router_service(cx: &ScopeState) -> Option<&Rc>> { - cx.use_hook(|_| cx.consume_state::>()) - .as_ref() -} - -/// This hould only be used once per app -/// -/// You can manually parse the route if you want, but the derived `parse` method on `Routable` will also work just fine -pub fn use_router(cx: &ScopeState, mut parse: impl FnMut(&str) -> R + 'static) -> &R { - // for the web, attach to the history api - let state = cx.use_hook( - #[cfg(not(feature = "web"))] - |_| {}, - #[cfg(feature = "web")] - |f| { - // - use gloo::events::EventListener; - - let base = window() - .unwrap() - .document() - .unwrap() - .query_selector("base[href]") - .ok() - .flatten() - .and_then(|base| { - let base = JsCast::unchecked_into::(base).href(); - let url = web_sys::Url::new(&base).unwrap(); - - if url.pathname() != "/" { - Some(strip_slash_suffix(&base).to_string()) - } else { - None - } - }); - - let location = window().unwrap().location(); - let pathname = location.pathname().unwrap(); - let initial_route = parse(&pathname); - - let service: RouterService = RouterService { - historic_routes: vec![initial_route], - history_service: RefCell::new( - web_sys::window().unwrap().history().expect("no history"), - ), - base_ur: RefCell::new(base), - }; - - // let base = base_url(); - // let url = route.to_path(); - // pending_routes: RefCell::new(vec![]), - // service.history_service.push_state(data, title); - - // cx.provide_state(service); - - let regenerate = cx.schedule_update(); - - // // when "back" is called by the user, we want to to re-render the component - let listener = EventListener::new(&web_sys::window().unwrap(), "popstate", move |_| { - // - regenerate(); - }); - - service - }, - ); - - let base = state.base_ur.borrow(); - if let Some(base) = base.as_ref() { - let path = format!("{}{}", base, state.get_current_route()); - } - - let history = state.history_service.borrow(); - state.historic_routes.last().unwrap() -} diff --git a/packages/router/src/routecontext.rs b/packages/router/src/routecontext.rs index fdd4f9d90..17d4cf850 100644 --- a/packages/router/src/routecontext.rs +++ b/packages/router/src/routecontext.rs @@ -1,7 +1,24 @@ +/// A `RouteContext` is a context that is provided by [`Route`](fn.Route.html) components. +/// +/// This signals to all child [`Route`] and [`Link`] components that they are +/// currently nested under this route. +#[derive(Debug, Clone)] pub struct RouteContext { - // "/name/:id" + /// The `declared_route` is the sub-piece of the route that matches this pattern. + /// + /// + /// It follows this pattern: + /// ``` + /// "name/:id" + /// ``` pub declared_route: String, - // "app/name/:id" + /// The `total_route` is the full route that matches this pattern. + /// + /// + /// It follows this pattern: + /// ``` + /// "/level0/level1/:id" + /// ``` pub total_route: String, } diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index ad45083ed..c33214c73 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,150 +1,202 @@ -use gloo::history::{BrowserHistory, History, HistoryListener, Location}; +// 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 dioxus_core::ScopeId; +use futures_channel::mpsc::UnboundedSender; +use std::any::Any; use std::{ - cell::{Cell, Ref, RefCell}, + cell::{Cell, RefCell}, collections::{HashMap, HashSet}, rc::Rc, sync::Arc, }; +use url::Url; -use dioxus_core::ScopeId; +/// An abstraction over the platform's history API. +/// +/// The history is denoted using web-like semantics, with forward slashes delmitiing +/// routes and question marks denoting optional parameters. +/// +/// This RouterService is exposed so you can modify the history directly. It +/// does not provide a high-level ergonomic API for your components. Instead, +/// you should consider using the components and hooks instead. +/// - [`Route`](struct.Route.html) +/// - [`Link`](struct.Link.html) +/// - [`UseRoute`](struct.UseRoute.html) +/// - [`Router`](struct.Router.html) +/// +/// +/// # Example +/// +/// ```rust +/// let router = Router::new(); +/// router.push_route("/home/custom"); +/// cx.provide_context(router); +/// ``` +/// +/// # Platform Specific +/// +/// - On the web, this is a [`BrowserHistory`](https://docs.rs/gloo/0.3.0/gloo/history/struct.BrowserHistory.html). +/// - 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>, -use crate::platform::RouterProvider; + pub stack: RefCell>>, -pub struct RouterService { - pub(crate) regen_route: Arc, - pub(crate) pending_events: Rc>>, - slots: Rc>>, - onchange_listeners: Rc>>, - root_found: Rc>>, - cur_path_params: Rc>>, + pub router_needs_update: Cell, - // history: Rc, - history: Rc>, - listener: HistoryListener, + pub tx: UnboundedSender, + + pub slots: Rc>>, + + pub onchange_listeners: Rc>>, + + pub query_listeners: Rc>>, + + pub semgment_listeners: Rc>>, + + pub history: Box, + + pub cfg: RouterCfg, } +pub type RouterService = Arc; + +#[derive(Debug)] pub enum RouteEvent { - Change, + Push(String), Pop, - Push, } -enum RouteSlot { - Routes { - // the partial route - partial: String, +impl RouterCore { + pub fn new(tx: UnboundedSender, cfg: RouterCfg) -> Arc { + #[cfg(feature = "web")] + let history = Box::new(web::new(tx.clone())); - // the total route - total: String, + #[cfg(not(feature = "web"))] + let history = Box::new(hash::create_router()); - // Connections to other routs - rest: Vec, - }, -} + let route = Arc::new(ParsedRoute::new(history.init_location())); -impl RouterService { - pub fn new(regen_route: Arc, root_scope: ScopeId) -> Self { - let history = BrowserHistory::default(); - let location = history.location(); - let path = location.path(); + Arc::new(Self { + cfg, + tx, + root_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(), + }) + } - let onchange_listeners = Rc::new(RefCell::new(HashSet::new())); - let slots: Rc>> = Default::default(); - let pending_events: Rc>> = Default::default(); - let root_found = Rc::new(Cell::new(None)); + pub fn handle_route_event(&self, msg: RouteEvent) -> Option> { + log::debug!("handling route event {:?}", msg); + self.root_found.set(None); - let listener = history.listen({ - let pending_events = pending_events.clone(); - let regen_route = regen_route.clone(); - let root_found = root_found.clone(); - let slots = slots.clone(); - let onchange_listeners = onchange_listeners.clone(); - move || { - root_found.set(None); - // checking if the route is valid is cheap, so we do it - for (slot, root) in slots.borrow_mut().iter().rev() { - regen_route(*slot); - } + match msg { + RouteEvent::Push(route) => { + let cur = self.current_location(); - for listener in onchange_listeners.borrow_mut().iter() { - regen_route(*listener); - } + let new_url = cur.url.join(&route).ok().unwrap(); - // also regenerate the root - regen_route(root_scope); + self.history.push(new_url.as_str()); - pending_events.borrow_mut().push(RouteEvent::Change) + 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 { - listener, - root_found, - history: Rc::new(RefCell::new(history)), - regen_route, - slots, - pending_events, - onchange_listeners, - cur_path_params: Rc::new(RefCell::new(HashMap::new())), + 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) { - self.history.borrow_mut().push(route); + // convert the users route to our internal format + self.tx + .unbounded_send(RouteEvent::Push(route.to_string())) + .unwrap(); } - pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { + /// 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().push((scope, clean)); + self.slots.borrow_mut().insert(scope, clean); } - pub fn should_render(&self, scope: ScopeId) -> bool { - if let Some(root_id) = self.root_found.get() { - if root_id == scope { - return true; - } - return false; - } + pub(crate) fn should_render(&self, scope: ScopeId) -> bool { + log::debug!("Checking render: {:?}", scope); - let location = self.history.borrow().location(); - let path = location.path(); + if let Some(root_id) = self.root_found.get() { + return root_id == scope; + } let roots = self.slots.borrow(); - let root = roots.iter().find(|(id, route)| id == &scope); + if let Some(route) = roots.get(&scope) { + log::debug!("Registration found for scope {:?} {:?}", scope, route); - // fallback logic - match root { - Some((id, route)) => { - if let Some(params) = route_matches_path(route, path) { - self.root_found.set(Some(*id)); - *self.cur_path_params.borrow_mut() = params; - true - } else { - if route == "" { - self.root_found.set(Some(*id)); - true - } else { - false - } - } + if route_matches_path(&self.current_location(), route) || route.is_empty() { + self.root_found.set(Some(scope)); + true + } else { + false } - None => false, + } else { + log::debug!("no route found for scope: {:?}", scope); + false } } - pub fn current_location(&self) -> Location { - self.history.borrow().location().clone() + /// Get the current location of the Router + pub fn current_location(&self) -> Arc { + self.stack.borrow().last().unwrap().clone() } - pub fn current_path_params(&self) -> Ref> { - self.cur_path_params.borrow() + pub fn query_current_location(&self) -> HashMap { + todo!() + // self.history.borrow().query() } + /// Get the current location of the Router + pub fn native_location(&self) -> Option> { + self.history.native_location().downcast::().ok() + } + + /// Registers a scope to regenerate on route change. + /// + /// This is useful if you've built some abstraction on top of the router service. pub fn subscribe_onchange(&self, id: ScopeId) { self.onchange_listeners.borrow_mut().insert(id); } + /// Unregisters a scope to regenerate on route change. + /// + /// This is useful if you've built some abstraction on top of the router service. pub fn unsubscribe_onchange(&self, id: ScopeId) { self.onchange_listeners.borrow_mut().remove(&id); } @@ -161,41 +213,128 @@ fn clean_path(path: &str) -> &str { if path == "/" { return path; } - path.trim_end_matches('/') + let sub = path.trim_end_matches('/'); + + if sub.starts_with('/') { + &path[1..] + } else { + sub + } } -fn route_matches_path(route: &str, path: &str) -> Option> { - let route_pieces = route.split('/').collect::>(); - let path_pieces = clean_path(path).split('/').collect::>(); +fn route_matches_path(cur: &ParsedRoute, attempt: &str) -> bool { + let cur_pieces = cur.url.path_segments().unwrap().collect::>(); + let attempt_pieces = clean_path(attempt).split('/').collect::>(); - if route_pieces.len() != path_pieces.len() { - return None; + if attempt == "/" && cur_pieces.len() == 1 && cur_pieces[0].is_empty() { + return true; } - let mut matches = HashMap::new(); - for (i, r) in route_pieces.iter().enumerate() { + 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(':') { - let param = &r[1..]; - matches.insert(param.to_string(), path_pieces[i].to_string()); continue; } - if path_pieces[i] != *r { - return None; + if cur_pieces[i] != *r { + return false; } } - Some(matches) + true } -pub struct RouterCfg { - initial_route: String, +pub trait RouterProvider { + fn push(&self, path: &str); + fn pop(&self); + fn native_location(&self) -> Box; + fn init_location(&self) -> Url; } -impl RouterCfg { - pub fn new(initial_route: String) -> Self { - Self { initial_route } +mod hash { + use super::*; + + /// a simple cross-platform hash-based router + pub struct HashRouter {} + + impl RouterProvider for HashRouter { + fn push(&self, _path: &str) {} + + fn native_location(&self) -> Box { + Box::new(()) + } + + fn pop(&self) {} + + fn init_location(&self) -> Url { + Url::parse("app:///").unwrap() + } + } +} + +#[cfg(feature = "web")] +mod web { + use super::RouterProvider; + use crate::RouteEvent; + + use futures_channel::mpsc::UnboundedSender; + use gloo::{ + events::EventListener, + history::{BrowserHistory, History}, + }; + use std::any::Any; + use url::Url; + + pub struct WebRouter { + // keep it around so it drops when the router is dropped + _listener: gloo::events::EventListener, + + history: BrowserHistory, + } + + 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 native_location(&self) -> Box { + todo!() + } + + fn pop(&self) { + // set the title, maybe? + } + + fn init_location(&self) -> Url { + url::Url::parse(&web_sys::window().unwrap().location().href().unwrap()).unwrap() + } + } + + pub fn new(tx: UnboundedSender) -> WebRouter { + WebRouter { + history: BrowserHistory::new(), + _listener: EventListener::new(&web_sys::window().unwrap(), "popstate", move |_| { + let _ = tx.unbounded_send(RouteEvent::Pop); + }), + } } } diff --git a/packages/router/tests/desktop_router.rs b/packages/router/tests/desktop_router.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/router/tests/ssr_router.rs b/packages/router/tests/ssr_router.rs new file mode 100644 index 000000000..71ff38e6d --- /dev/null +++ b/packages/router/tests/ssr_router.rs @@ -0,0 +1,29 @@ +#![allow(non_snake_case)] + +use dioxus_core::prelude::*; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; +use dioxus_router::*; + +#[test] +fn generates_without_error() { + let mut app = VirtualDom::new(app); + app.rebuild(); + + let out = dioxus_ssr::render_vdom(&app); + + assert_eq!(out, "

Home

"); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + Router { + nav { "navbar" } + Route { to: "/home", Home {} } + } + }) +} + +fn Home(cx: Scope) -> Element { + cx.render(rsx! { h1 { "Home" } }) +} diff --git a/packages/router/tests/route.rs b/packages/router/tests/web_router.rs similarity index 100% rename from packages/router/tests/route.rs rename to packages/router/tests/web_router.rs diff --git a/tests/sharedstate.rs b/tests/sharedstate.rs index a4eab6b10..e07baaaed 100644 --- a/tests/sharedstate.rs +++ b/tests/sharedstate.rs @@ -2,6 +2,7 @@ use dioxus::prelude::*; use dioxus_core::{DomEdit, Mutations, SchedulerMsg, ScopeId}; +use std::rc::Rc; use DomEdit::*; mod test_logging; @@ -11,12 +12,12 @@ fn shared_state_test() { struct MySharedState(&'static str); static App: Component = |cx| { - cx.provide_context(MySharedState("world!")); + cx.provide_context(Rc::new(MySharedState("world!"))); cx.render(rsx!(Child {})) }; static Child: Component = |cx| { - let shared = cx.consume_context::()?; + let shared = cx.consume_context::>()?; cx.render(rsx!("Hello, {shared.0}")) }; @@ -40,7 +41,7 @@ fn swap_test() { let val = cx.use_hook(|_| 0); *val += 1; - cx.provide_context(MySharedState("world!")); + cx.provide_context(Rc::new(MySharedState("world!"))); let child = match *val % 2 { 0 => rsx!( @@ -71,7 +72,7 @@ fn swap_test() { #[inline_props] fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element { - let shared = cx.consume_context::().unwrap(); + let shared = cx.consume_context::>().unwrap(); println!("Child1: {}", shared.0); cx.render(rsx! { div { @@ -83,7 +84,7 @@ fn swap_test() { #[inline_props] fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element { - let shared = cx.consume_context::().unwrap(); + let shared = cx.consume_context::>().unwrap(); println!("Child2: {}", shared.0); cx.render(rsx! { h1 { From 60b11081f20bcb770cc75fdfbbb631b5f1211bdc Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Mar 2022 18:43:21 -0500 Subject: [PATCH 02/14] chore: remove idna feature from url --- packages/router/Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 6065fdddd..fc51febc2 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -14,10 +14,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } -# serde = "1" -# serde_urlencoded = "0.7" futures-channel = "0.3.21" -url = "2.2.2" +url = { version ="2.2.2", default-features = false } # for wasm web-sys = { version = "0.3", features = [ From cc839003340a10ada1aa597217d20650a6388b42 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Mar 2022 22:35:57 -0500 Subject: [PATCH 03/14] feat: router crate is polished up --- packages/router/Cargo.toml | 4 +- packages/router/examples/simple.rs | 7 +- packages/router/src/components/link.rs | 2 +- packages/router/src/components/redirect.rs | 42 ++- packages/router/src/components/router.rs | 77 ++++-- packages/router/src/hooks/use_param.rs | 5 - packages/router/src/hooks/use_query.rs | 0 packages/router/src/hooks/use_route.rs | 89 +++++- packages/router/src/lib.rs | 6 - packages/router/src/location.rs | 66 ----- packages/router/src/service.rs | 297 +++++++++++---------- packages/router/src/utils.rs | 6 - 12 files changed, 341 insertions(+), 260 deletions(-) delete mode 100644 packages/router/src/hooks/use_param.rs delete mode 100644 packages/router/src/hooks/use_query.rs delete mode 100644 packages/router/src/location.rs delete mode 100644 packages/router/src/utils.rs diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index fc51febc2..b7eb6d64d 100644 --- a/packages/router/Cargo.toml +++ b/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] diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple.rs index 6c559494d..9ac8b3e3b 100644 --- a/packages/router/examples/simple.rs +++ b/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::("id")?; + let id = use_route(&cx).segment("id")?; + + log::debug!("rendering blog post {}", id); + cx.render(rsx! { div { "{id:?}" } }) } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 3699e0a2a..f17a203af 100644 --- a/packages/router/src/components/link.rs +++ b/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", diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index f9b409e25..d99ca5088 100644 --- a/packages/router/src/components/redirect.rs +++ b/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 } diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index e3cbec0fd..37899d19c 100644 --- a/packages/router/src/components/router.rs +++ b/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::(); @@ -57,23 +47,62 @@ 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); + 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, + }); - regen_route(router_id); - - for listener in svc.onchange_listeners.borrow().iter() { - regen_route(*listener); + svc.history.push(&new_route); + svc.stack.borrow_mut().push(new_route); } - for route in svc.slots.borrow().keys() { - regen_route(*route); + 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; } + + 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)) diff --git a/packages/router/src/hooks/use_param.rs b/packages/router/src/hooks/use_param.rs deleted file mode 100644 index da0aad89b..000000000 --- a/packages/router/src/hooks/use_param.rs +++ /dev/null @@ -1,5 +0,0 @@ -use dioxus_core::prelude::*; - -pub fn use_param(cx: &ScopeState) -> Option { - todo!() -} diff --git a/packages/router/src/hooks/use_query.rs b/packages/router/src/hooks/use_query.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 435d54920..651a5dc1f 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/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::>() + .consume_context::() + .expect("Cannot call use_route outside the scope of a Router component"); + + let route_context = cx + .consume_context::() .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, + 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> { + 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::()`. 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::()`. This method returns `None` if the named + /// parameter does not exist in the current path. + pub fn parse_segment(&self, name: &str) -> Option> + where + T: FromStr, + { + self.segment(name).map(|value| value.parse::()) + } } // 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, + state: UseRoute, router: Arc, scope: ScopeId, } diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index 78b310c7f..5aaac6ad3 100644 --- a/packages/router/src/lib.rs +++ b/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::*; diff --git a/packages/router/src/location.rs b/packages/router/src/location.rs deleted file mode 100644 index 64cb66522..000000000 --- a/packages/router/src/location.rs +++ /dev/null @@ -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::()`. This method returns `None` if the named - /// parameter does not exist in the current path. - pub fn segment(&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::()`. This method returns `None` if the named - /// parameter does not exist in the current path. - pub fn parse_segment(&self, name: &str) -> Option> - where - T: FromStr, - { - self.url - .path_segments()? - .find(|&f| f.eq(name)) - .map(|f| f.parse::()) - } -} - -#[test] -fn parses_location() { - let route = ParsedRoute::new(Url::parse("app:///foo/bar?baz=qux&quux=corge").unwrap()); -} diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index c33214c73..28e425430 100644 --- a/packages/router/src/service.rs +++ b/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>, + pub(crate) route_found: Cell>, - pub stack: RefCell>>, + pub(crate) stack: RefCell>>, - pub router_needs_update: Cell, + pub(crate) tx: UnboundedSender, - pub tx: UnboundedSender, + pub(crate) slots: Rc>>, - pub slots: Rc>>, + pub(crate) onchange_listeners: Rc>>, - pub onchange_listeners: Rc>>, + pub(crate) history: Box, - pub query_listeners: Rc>>, - - pub semgment_listeners: Rc>>, - - pub history: Box, - - pub cfg: RouterCfg, + pub(crate) cfg: RouterCfg, } +/// A shared type for the RouterCore. pub type RouterService = Arc; +/// 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, + + /// The title of the route. + pub title: Option, + + /// The serialized state of the route. + pub serialized_state: Option, +} + #[derive(Debug)] -pub enum RouteEvent { - Push(String), +pub(crate) enum RouteEvent { + Push { + route: String, + title: Option, + serialized_state: Option, + }, + Replace { + route: String, + title: Option, + serialized_state: Option, + }, Pop, } impl RouterCore { - pub fn new(tx: UnboundedSender, cfg: RouterCfg) -> Arc { + pub(crate) fn new(tx: UnboundedSender, cfg: RouterCfg) -> Arc { #[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> { - 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, serialized_state: Option) { + 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(); + let _ = self.tx.unbounded_send(RouteEvent::Pop); } - 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 { - 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, + serialized_state: Option, + ) { + 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 { - todo!() - // self.history.borrow().query() - } - - /// Get the current location of the Router + /// Get the current native location of the Router pub fn native_location(&self) -> Option> { self.history.native_location().downcast::().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::>(); +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::>(), + None => cur_piece_iter.collect::>(), + }; + let attempt_pieces = clean_path(attempt).split('/').collect::>(); 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; - 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 { 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 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 native_location(&self) -> Box { - todo!() + Box::new(self.window.location()) } - fn pop(&self) { - // set the title, maybe? - } - - 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) -> WebRouter { + pub(crate) fn new(tx: UnboundedSender) -> 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); }), diff --git a/packages/router/src/utils.rs b/packages/router/src/utils.rs deleted file mode 100644 index 48afe0d80..000000000 --- a/packages/router/src/utils.rs +++ /dev/null @@ -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) -} From 06d4a5640b02cb97a58c0bcde45cda5dd9e2e60a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Mar 2022 22:38:38 -0500 Subject: [PATCH 04/14] docs: fix doc comment --- packages/router/src/components/redirect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index d99ca5088..b4261b0d4 100644 --- a/packages/router/src/components/redirect.rs +++ b/packages/router/src/components/redirect.rs @@ -18,7 +18,7 @@ pub struct RedirectProps<'a> { /// ``` pub to: &'a str, - /// The route to link to. This can be a relative path, or a full URL. + /// The route to link from. This can be a relative path, or a full URL. /// /// ```rust /// // Absolute path From c7627f0b93b8e9024063d5734409bf8d529cf800 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Mar 2022 23:03:10 -0500 Subject: [PATCH 05/14] fix: tests pass --- examples/router.rs | 5 +++-- packages/router/Cargo.toml | 12 +++++++----- packages/router/README.md | 2 +- packages/router/src/components/link.rs | 4 ++-- packages/router/src/components/redirect.rs | 4 ++-- packages/router/src/components/route.rs | 2 +- packages/router/src/hooks/use_route.rs | 11 ++++++++++- packages/router/src/routecontext.rs | 4 ++-- packages/router/src/service.rs | 4 +++- packages/router/tests/ssr_router.rs | 3 ++- 10 files changed, 33 insertions(+), 18 deletions(-) diff --git a/examples/router.rs b/examples/router.rs index 216b69922..0dc09f566 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -31,7 +31,7 @@ fn app(cx: Scope) -> Element { } fn BlogPost(cx: Scope) -> Element { - let post = dioxus::router::use_route(&cx).last_segment(); + let post = dioxus::router::use_route(&cx).last_segment()?; cx.render(rsx! { div { @@ -47,7 +47,8 @@ struct Query { } fn User(cx: Scope) -> Element { - let post = dioxus::router::use_route(&cx).last_segment(); + let post = dioxus::router::use_route(&cx).last_segment()?; + let query = dioxus::router::use_route(&cx) .query::() .unwrap_or(Query { bold: false }); diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index b7eb6d64d..61c52341c 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -35,12 +35,13 @@ gloo-events = { version = "0.1.1", optional = true } log = "0.4.14" thiserror = "1.0.30" futures-util = "0.3.21" - +serde = { version = "1", optional = true } +serde_urlencoded = {version = "0.7.1", optional = true} [features] -default = ["web"] +default = ["query"] web = ["web-sys", "gloo-events", "js-sys", "wasm-bindgen"] -hash = [] +query = ["serde"] [dev-dependencies] console_error_panic_hook = "0.1.7" @@ -50,10 +51,11 @@ wasm-logger = "0.2.0" wasm-bindgen-test = "0.3" gloo-utils = "0.1.2" dioxus-ssr = { path = "../ssr"} + dioxus-router = { path = ".", default-features = false } -# [target.wasm32-unknown-unknown.dev-dependencies] -# dioxus-router = { path = ".", features = ["web"] } +[target.wasm32-unknown-unknown.dev-dependencies] +dioxus-router = { path = ".", features = ["web"] } diff --git a/packages/router/README.md b/packages/router/README.md index 7cfdb1332..d0c036bc9 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -51,7 +51,7 @@ Dioxus Router is a first-party Router for all your Dioxus Apps. It provides a React-Router style interface that works anywhere: across the browser, SSR, and natively. -```rust +```rust, ignore fn app() { cx.render(rsx! { Router { diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index f17a203af..3c03bb3bf 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -11,7 +11,7 @@ use dioxus_html as dioxus_elements; pub struct LinkProps<'a> { /// The route to link to. This can be a relative path, or a full URL. /// - /// ```rust + /// ```rust, ignore /// // Absolute path /// Link { to: "/home", "Go Home" } /// @@ -64,7 +64,7 @@ pub struct LinkProps<'a> { /// /// # Examples /// -/// ```rust +/// ```rust, ignore /// fn Header(cx: Scope) -> Element { /// cx.render(rsx!{ /// Link { to: "/home", "Go Home" } diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index b4261b0d4..aae8e6d92 100644 --- a/packages/router/src/components/redirect.rs +++ b/packages/router/src/components/redirect.rs @@ -9,7 +9,7 @@ use crate::use_router; pub struct RedirectProps<'a> { /// The route to link to. This can be a relative path, or a full URL. /// - /// ```rust + /// ```rust, ignore /// // Absolute path /// Redirect { from: "", to: "/home" } /// @@ -20,7 +20,7 @@ pub struct RedirectProps<'a> { /// The route to link from. This can be a relative path, or a full URL. /// - /// ```rust + /// ```rust, ignore /// // Absolute path /// Redirect { from: "", to: "/home" } /// diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 00fa28af2..530ad77a0 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -22,7 +22,7 @@ pub struct RouteProps<'a> { /// /// # Example /// -///```rust +///```rust, ignore /// rsx!( /// Router { /// Route { to: "/home", Home {} } diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 651a5dc1f..38fdd5fda 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -48,7 +48,16 @@ impl UseRoute { /// 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> { + #[cfg(feature = "query")] + pub fn query(&self) -> Option { + let query = self.url().query()?; + serde_urlencoded::from_str(query.strip_prefix('?').unwrap_or("")).ok() + } + + /// 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_param(&self, param: &str) -> Option> { self.route .url .query_pairs() diff --git a/packages/router/src/routecontext.rs b/packages/router/src/routecontext.rs index 17d4cf850..81383227b 100644 --- a/packages/router/src/routecontext.rs +++ b/packages/router/src/routecontext.rs @@ -8,7 +8,7 @@ pub struct RouteContext { /// /// /// It follows this pattern: - /// ``` + /// ```ignore /// "name/:id" /// ``` pub declared_route: String, @@ -17,7 +17,7 @@ pub struct RouteContext { /// /// /// It follows this pattern: - /// ``` + /// ```ignore /// "/level0/level1/:id" /// ``` pub total_route: String, diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 28e425430..5cf93c597 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -29,7 +29,7 @@ use url::Url; /// /// # Example /// -/// ```rust +/// ```rust, ignore /// let router = Router::new(); /// router.push_route("/home/custom"); /// cx.provide_context(router); @@ -280,6 +280,8 @@ mod hash { serialized_state: None, } } + + fn replace(&self, _route: &ParsedRoute) {} } } diff --git a/packages/router/tests/ssr_router.rs b/packages/router/tests/ssr_router.rs index 71ff38e6d..212daa7cc 100644 --- a/packages/router/tests/ssr_router.rs +++ b/packages/router/tests/ssr_router.rs @@ -12,13 +12,14 @@ fn generates_without_error() { let out = dioxus_ssr::render_vdom(&app); - assert_eq!(out, "

Home

"); + assert_eq!(out, "default"); } fn app(cx: Scope) -> Element { cx.render(rsx! { Router { nav { "navbar" } + Route { to: "/", "default" } Route { to: "/home", Home {} } } }) From 82f69a9ca6f57213fb16f1b6ddbac447a1eeb41b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Mar 2022 23:08:05 -0500 Subject: [PATCH 06/14] fix: feature flagging --- packages/router/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 61c52341c..8666df4e1 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -41,7 +41,7 @@ serde_urlencoded = {version = "0.7.1", optional = true} [features] default = ["query"] web = ["web-sys", "gloo-events", "js-sys", "wasm-bindgen"] -query = ["serde"] +query = ["serde", "serde_urlencoded"] [dev-dependencies] console_error_panic_hook = "0.1.7" From 9edc3459d5736ccf3849ca6bbdb4333ad0d15ef8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Mar 2022 23:31:20 -0500 Subject: [PATCH 07/14] fix: remove latent test --- packages/router/tests/desktop_router.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/router/tests/desktop_router.rs diff --git a/packages/router/tests/desktop_router.rs b/packages/router/tests/desktop_router.rs deleted file mode 100644 index e69de29bb..000000000 From 57bd8b7ce2d96554d225c15cb5f41f656d7cb596 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Mar 2022 23:45:36 -0500 Subject: [PATCH 08/14] fix: enable wasmtest feature --- packages/router/Cargo.toml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 8666df4e1..adbedb714 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -15,7 +15,7 @@ dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } futures-channel = "0.3.21" -url = { version ="2.2.2", default-features = false } +url = { version = "2.2.2", default-features = false } # for wasm web-sys = { version = "0.3", features = [ @@ -36,12 +36,13 @@ log = "0.4.14" thiserror = "1.0.30" futures-util = "0.3.21" serde = { version = "1", optional = true } -serde_urlencoded = {version = "0.7.1", optional = true} +serde_urlencoded = { version = "0.7.1", optional = true } [features] default = ["query"] web = ["web-sys", "gloo-events", "js-sys", "wasm-bindgen"] query = ["serde", "serde_urlencoded"] +wasm_test = [] [dev-dependencies] console_error_panic_hook = "0.1.7" @@ -50,16 +51,13 @@ log = "0.4.14" wasm-logger = "0.2.0" wasm-bindgen-test = "0.3" gloo-utils = "0.1.2" -dioxus-ssr = { path = "../ssr"} +dioxus-ssr = { path = "../ssr" } dioxus-router = { path = ".", default-features = false } [target.wasm32-unknown-unknown.dev-dependencies] dioxus-router = { path = ".", features = ["web"] } - - - [dev-dependencies.web-sys] version = "0.3" features = ["Document"] From cdbc37711fa9c64b2cfa767766ca55c95c1736f7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 3 Mar 2022 11:23:01 -0500 Subject: [PATCH 09/14] fix: wasm test --- packages/router/tests/web_router.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router/tests/web_router.rs b/packages/router/tests/web_router.rs index 89b7a64fa..834baad73 100644 --- a/packages/router/tests/web_router.rs +++ b/packages/router/tests/web_router.rs @@ -21,7 +21,7 @@ fn simple_test() { static APP: Component = |cx| { cx.render(rsx! { Router { - onchange: move |route| log::info!("route changed to {}", route), + onchange: move |route: RouterService| log::info!("route changed to {:?}", route.current_location()), Route { to: "/", Home {} } Route { to: "blog" Route { to: "/", BlogList {} } @@ -48,7 +48,7 @@ fn simple_test() { } fn BlogPost(cx: Scope) -> Element { - let id = use_route(&cx).segment::("id")?; + let id = use_route(&cx).parse_segment::("id")?; cx.render(rsx! { div { From c19995fed946016b75507d31b873dab2346adfbd Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Mar 2022 13:08:25 -0500 Subject: [PATCH 10/14] fix: debug multistage edits in desktop --- Cargo.toml | 2 +- examples/flat_router.rs | 40 ++++++++++++++++++++++++++++++++ packages/core/src/scopes.rs | 2 +- packages/core/src/virtual_dom.rs | 2 ++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 examples/flat_router.rs diff --git a/Cargo.toml b/Cargo.toml index eb098313f..55ed3bdac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi"] } fern = { version = "0.6.0", features = ["colored"] } criterion = "0.3.5" thiserror = "1.0.30" - +env_logger = "0.9.0" [[bench]] name = "create" diff --git a/examples/flat_router.rs b/examples/flat_router.rs new file mode 100644 index 000000000..af408a685 --- /dev/null +++ b/examples/flat_router.rs @@ -0,0 +1,40 @@ +use dioxus::prelude::*; + +use dioxus::router::*; + +use dioxus::desktop::tao::dpi::LogicalSize; + +fn main() { + env_logger::init(); + + dioxus::desktop::launch_cfg(app, |c| { + c.with_window(|c| { + c.with_title("Spinsense Client") + .with_inner_size(LogicalSize::new(600, 1000)) + .with_resizable(false) + }) + }) +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + Router { + Route { to: "/", "Home" } + Route { to: "/games", "Games" } + Route { to: "/play", "Play" } + Route { to: "/settings", "Settings" } + + p { + "----" + } + nav { + ul { + Link { to: "/", li { "Home" } } + Link { to: "/games", li { "Games" } } + Link { to: "/play", li { "Play" } } + Link { to: "/settings", li { "Settings" } } + } + } + } + }) +} diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ce1982e4f..010c63b03 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -440,7 +440,7 @@ impl<'a, P> std::ops::Deref for Scope<'a, P> { /// `ScopeId` is a `usize` that is unique across the entire VirtualDOM and across time. ScopeIDs will never be reused /// once a component has been unmounted. #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct ScopeId(pub usize); /// A task's unique identifier. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 8d3fd1789..025262172 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -515,6 +515,8 @@ impl VirtualDom { } } + println!("committed_mutations: {:?}", committed_mutations); + committed_mutations } From c466425b82d716f31d29c00cb789fe87581ff6a1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Mar 2022 13:12:30 -0500 Subject: [PATCH 11/14] fix: remove logging --- packages/core/src/virtual_dom.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 025262172..8d3fd1789 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -515,8 +515,6 @@ impl VirtualDom { } } - println!("committed_mutations: {:?}", committed_mutations); - committed_mutations } From dbcd91e32f13b6d95de926528765c60932809cdd Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Mar 2022 14:28:21 -0500 Subject: [PATCH 12/14] chore: switch to mutex --- packages/desktop/src/controller.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 83246b14e..8a1494e52 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -2,8 +2,8 @@ use crate::desktop_context::{DesktopContext, UserWindowEvent}; use dioxus_core::*; use std::{ collections::HashMap, - sync::atomic::AtomicBool, - sync::{Arc, RwLock}, + sync::Arc, + sync::{atomic::AtomicBool, Mutex}, }; use wry::{ self, @@ -14,7 +14,7 @@ use wry::{ pub(super) struct DesktopController { pub(super) webviews: HashMap, pub(super) sender: futures_channel::mpsc::UnboundedSender, - pub(super) pending_edits: Arc>>, + pub(super) pending_edits: Arc>>, pub(super) quit_app_on_close: bool, pub(super) is_ready: Arc, } @@ -27,13 +27,13 @@ impl DesktopController { props: P, proxy: EventLoopProxy, ) -> Self { - let edit_queue = Arc::new(RwLock::new(Vec::new())); - let pending_edits = edit_queue.clone(); - + let edit_queue = Arc::new(Mutex::new(Vec::new())); let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let return_sender = sender.clone(); + let pending_edits = edit_queue.clone(); + let return_sender = sender.clone(); let desktop_context_proxy = proxy.clone(); + std::thread::spawn(move || { // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads let runtime = tokio::runtime::Builder::new_multi_thread() @@ -52,7 +52,7 @@ impl DesktopController { let edits = dom.rebuild(); edit_queue - .write() + .lock() .unwrap() .push(serde_json::to_string(&edits.edits).unwrap()); @@ -62,9 +62,10 @@ impl DesktopController { loop { dom.wait_for_work().await; let mut muts = dom.work_with_deadline(|| false); + while let Some(edit) = muts.pop() { edit_queue - .write() + .lock() .unwrap() .push(serde_json::to_string(&edit.edits).unwrap()); } @@ -93,7 +94,7 @@ impl DesktopController { pub(super) fn try_load_ready_webviews(&mut self) { if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut queue = self.pending_edits.write().unwrap(); + let mut queue = self.pending_edits.lock().unwrap(); let (_id, view) = self.webviews.iter_mut().next().unwrap(); while let Some(edit) = queue.pop() { From 83be3df42a265d24fc8fa63fb6a7acff54c40221 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Mar 2022 14:34:25 -0500 Subject: [PATCH 13/14] cfg: expose router feature flags for web --- Cargo.toml | 4 ++-- src/lib.rs | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 55ed3bdac..6e514dbe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,12 +31,12 @@ dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.0.0", o [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro"] # macro = ["dioxus-core-macro", "dioxus-rsx"] +macro = ["dioxus-core-macro"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] -web = ["dioxus-web"] +web = ["dioxus-web", "dioxus-router/web"] desktop = ["dioxus-desktop"] ayatana = ["dioxus-desktop/ayatana"] router = ["dioxus-router"] diff --git a/src/lib.rs b/src/lib.rs index 339f9693d..ebcc0d93e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,9 +20,6 @@ pub use dioxus_desktop as desktop; #[cfg(feature = "fermi")] pub use fermi; -// #[cfg(feature = "mobile")] -// pub use dioxus_mobile as mobile; - pub mod events { #[cfg(feature = "html")] pub use dioxus_html::{on::*, KeyCode}; @@ -34,4 +31,10 @@ pub mod prelude { pub use dioxus_elements::{GlobalAttributes, SvgAttributes}; pub use dioxus_hooks::*; pub use dioxus_html as dioxus_elements; + + #[cfg(feature = "router")] + pub use dioxus_router::{use_route, use_router, Link, Redirect, Router, UseRoute}; + + #[cfg(feature = "fermi")] + pub use fermi::{use_atom_ref, use_init_atom_root, use_read, use_set, Atom, AtomRef}; } From f47d2900fb77fd155e9525afe6cf3ca4f363b218 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 5 Mar 2022 14:06:54 -0500 Subject: [PATCH 14/14] feat: add a redirect --- examples/fermi.rs | 1 - packages/router/examples/simple.rs | 1 + packages/router/src/components/redirect.rs | 14 ++++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/fermi.rs b/examples/fermi.rs index 2be01d019..f4391de5e 100644 --- a/examples/fermi.rs +++ b/examples/fermi.rs @@ -1,7 +1,6 @@ #![allow(non_snake_case)] use dioxus::prelude::*; -use fermi::prelude::*; fn main() { dioxus::desktop::launch(app) diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple.rs index 9ac8b3e3b..a079a5f5d 100644 --- a/packages/router/examples/simple.rs +++ b/packages/router/examples/simple.rs @@ -20,6 +20,7 @@ fn app(cx: Scope) -> Element { Link { to: "/blog", li { "blog" }} Link { to: "/blog/tim", li { "tims' blog" }} Link { to: "/blog/bill", li { "bills' blog" }} + Link { to: "/apples", li { "go to apples" }} } Route { to: "/", Home {} } Route { to: "/blog/", BlogList {} } diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index aae8e6d92..a1d2d7727 100644 --- a/packages/router/src/components/redirect.rs +++ b/packages/router/src/components/redirect.rs @@ -37,8 +37,18 @@ pub struct RedirectProps<'a> { 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); + let immediate_redirect = cx.use_hook(|_| { + if let Some(from) = cx.props.from { + router.register_total_route(from.to_string(), cx.scope_id()); + false + } else { + true + } + }); + + if *immediate_redirect || router.should_render(cx.scope_id()) { + router.replace_route(cx.props.to, None, None); + } None }