From cb2782b4bb34cdaadfff590bfee930ae3ac6536c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 5 Jan 2022 00:27:22 -0500 Subject: [PATCH] wip: memoize dom in the prescence of identical components --- Cargo.toml | 4 +- packages/core-macro/src/rsx/component.rs | 30 +++++--- packages/core-macro/src/rsx/element.rs | 2 +- packages/core/src/diff.rs | 28 +++---- packages/core/src/lazynodes.rs | 70 ++++++++--------- packages/core/src/nodes.rs | 33 ++++++-- packages/core/src/scopes.rs | 16 +++- packages/core/src/virtual_dom.rs | 5 +- packages/core/tests/miri_stress.rs | 30 +++++++- packages/hooks/src/usestate/owned.rs | 9 ++- packages/html/src/elements.rs | 5 ++ packages/router/.vscode/settings.json | 3 + packages/router/Cargo.toml | 6 +- packages/router/README.md | 53 ++++++------- packages/router/examples/simple.rs | 55 ++++++++------ packages/router/src/{ => components}/link.rs | 25 +++++-- packages/router/src/components/route.rs | 58 ++++++++++++++ packages/router/src/components/router.rs | 29 +++++++ packages/router/src/hooks/use_route.rs | 30 ++++++++ packages/router/src/lib.rs | 45 +++++------ packages/router/src/platform.rs | 7 -- packages/router/src/platform/mod.rs | 11 +++ packages/router/src/routecontext.rs | 7 ++ packages/router/src/service.rs | 79 +++++++++++++++++--- packages/router/src/userouter.rs | 33 -------- packages/web/src/dom.rs | 16 +++- packages/web/src/lib.rs | 4 + 27 files changed, 474 insertions(+), 219 deletions(-) create mode 100644 packages/router/.vscode/settings.json rename packages/router/src/{ => components}/link.rs (62%) create mode 100644 packages/router/src/components/route.rs create mode 100644 packages/router/src/components/router.rs create mode 100644 packages/router/src/hooks/use_route.rs delete mode 100644 packages/router/src/platform.rs create mode 100644 packages/router/src/platform/mod.rs create mode 100644 packages/router/src/routecontext.rs delete mode 100644 packages/router/src/userouter.rs diff --git a/Cargo.toml b/Cargo.toml index 3f92e2c62..e58d54825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ dioxus-web = { path = "./packages/web", version = "^0.0.3", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.3", optional = true } dioxus-ssr = { path = "./packages/ssr", version = "0.1.1", optional = true } -# dioxus-router = { path = "./packages/router", optional = true } +dioxus-router = { path = "./packages/router", optional = true } dioxus-mobile = { path = "./packages/mobile", version = "^0.0.2", optional = true } # dioxus-liveview = { path = "./packages/liveview", optional = true } @@ -33,7 +33,7 @@ html = ["dioxus-html"] ssr = ["dioxus-ssr"] web = ["dioxus-web"] desktop = ["dioxus-desktop"] -# router = ["dioxus-router"] +router = ["dioxus-router"] # "dioxus-router/web" # "dioxus-router/desktop" diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index fe8475131..3b1097536 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -19,7 +19,7 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, Ident, Result, Token, + token, Expr, Ident, LitStr, Result, Token, }; pub struct Component { @@ -135,8 +135,6 @@ impl ToTokens for Component { None => quote! { None }, }; - // #children - tokens.append_all(quote! { __cx.component( #name, @@ -155,7 +153,7 @@ pub struct ComponentField { enum ContentField { ManExpr(Expr), - + Formatted(LitStr), OnHandlerRaw(Expr), } @@ -163,6 +161,9 @@ impl ToTokens for ContentField { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { ContentField::ManExpr(e) => e.to_tokens(tokens), + ContentField::Formatted(s) => tokens.append_all(quote! { + __cx.raw_text(format_args_f!(#s)).0 + }), ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { __cx.bump().alloc(#e) }), @@ -175,13 +176,22 @@ impl Parse for ComponentField { let name = Ident::parse_any(input)?; input.parse::()?; - let name_str = name.to_string(); - let content = if name_str.starts_with("on") { - ContentField::OnHandlerRaw(input.parse()?) - } else { - ContentField::ManExpr(input.parse::()?) - }; + if name.to_string().starts_with("on") { + let content = ContentField::OnHandlerRaw(input.parse()?); + return Ok(Self { name, content }); + } + if name.to_string() == "key" { + let content = ContentField::ManExpr(input.parse()?); + return Ok(Self { name, content }); + } + + if input.peek(LitStr) && input.peek2(Token![,]) { + let content = ContentField::Formatted(input.parse()?); + return Ok(Self { name, content }); + } + + let content = ContentField::ManExpr(input.parse()?); Ok(Self { name, content }) } } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 71e0e90ac..a85b46694 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -46,7 +46,7 @@ impl Parse for Element { content.parse::()?; - if content.peek(LitStr) { + if content.peek(LitStr) && content.peek2(Token![,]) { let value = content.parse::()?; attributes.push(ElementAttrNamed { el_name: el_name.clone(), diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 4f8af6601..2d4111c43 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -390,10 +390,7 @@ impl<'bump> DiffState<'bump> { self.stack.add_child_count(1); if let Some(cur_scope_id) = self.stack.current_scope() { - let scope = self.scopes.get_scope(cur_scope_id).unwrap(); - for listener in *listeners { - self.attach_listener_to_scope(listener, scope); listener.mounted_node.set(Some(real_id)); self.mutations.new_event_listener(listener, cur_scope_id); } @@ -446,9 +443,9 @@ impl<'bump> DiffState<'bump> { } false => { // track this component internally so we know the right drop order - let cur_scope = self.scopes.get_scope(parent_idx).unwrap(); - let extended = unsafe { std::mem::transmute(vcomponent) }; - cur_scope.items.borrow_mut().borrowed_props.push(extended); + // let cur_scope = self.scopes.get_scope(parent_idx).unwrap(); + // let extended = unsafe { std::mem::transmute(vcomponent) }; + // cur_scope.items.borrow_mut().borrowed_props.push(extended); } } @@ -580,8 +577,6 @@ impl<'bump> DiffState<'bump> { // // TODO: take a more efficient path than this if let Some(cur_scope_id) = self.stack.current_scope() { - let scope = self.scopes.get_scope(cur_scope_id).unwrap(); - if old.listeners.len() == new.listeners.len() { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { if old_l.event != new_l.event { @@ -590,7 +585,6 @@ impl<'bump> DiffState<'bump> { self.mutations.new_event_listener(new_l, cur_scope_id); } new_l.mounted_node.set(old_l.mounted_node.get()); - self.attach_listener_to_scope(new_l, scope); } } else { for listener in old.listeners { @@ -600,7 +594,6 @@ impl<'bump> DiffState<'bump> { for listener in new.listeners { listener.mounted_node.set(Some(root)); self.mutations.new_event_listener(listener, cur_scope_id); - self.attach_listener_to_scope(listener, scope); } } } @@ -687,6 +680,12 @@ impl<'bump> DiffState<'bump> { new: &'bump VComponent<'bump>, ) { let scope_addr = old.scope.get().unwrap(); + log::debug!("diff_component_nodes: {:?}", scope_addr); + + if std::ptr::eq(old, new) { + log::debug!("skipping component diff - component is the sames"); + return; + } // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { @@ -728,11 +727,14 @@ impl<'bump> DiffState<'bump> { // this should auto drop the previous props self.scopes.run_scope(scope_addr); + self.mutations.dirty_scopes.insert(scope_addr); + self.diff_node( self.scopes.wip_head(scope_addr), self.scopes.fin_head(scope_addr), ); } else { + log::debug!("memoized"); // memoization has taken place drop(new_props); }; @@ -1293,10 +1295,4 @@ impl<'bump> DiffState<'bump> { } } } - - /// Adds a listener closure to a scope during diff. - fn attach_listener_to_scope(&mut self, listener: &'bump Listener<'bump>, scope: &ScopeState) { - let long_listener = unsafe { std::mem::transmute(listener) }; - scope.items.borrow_mut().listeners.push(long_listener) - } } diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index bd551da50..b22d09100 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -244,15 +244,15 @@ fn round_to_words(len: usize) -> usize { fn it_works() { let bump = bumpalo::Bump::new(); - let factory = NodeFactory { bump: &bump }; + // let factory = NodeFactory { bump: &bump }; - let caller = LazyNodes::new_some(|f| { - // - f.text(format_args!("hello world!")) - }); - let g = caller.call(factory); + // let caller = LazyNodes::new_some(|f| { + // // + // f.text(format_args!("hello world!")) + // }); + // let g = caller.call(factory); - dbg!(g); + // dbg!(g); } #[test] @@ -260,37 +260,37 @@ fn it_drops() { use std::rc::Rc; let bump = bumpalo::Bump::new(); - let factory = NodeFactory { bump: &bump }; + // let factory = NodeFactory { bump: &bump }; - struct DropInner { - id: i32, - } - impl Drop for DropInner { - fn drop(&mut self) { - log::debug!("dropping inner"); - } - } - let val = Rc::new(10); + // struct DropInner { + // id: i32, + // } + // impl Drop for DropInner { + // fn drop(&mut self) { + // log::debug!("dropping inner"); + // } + // } + // let val = Rc::new(10); - let caller = { - let it = (0..10) - .map(|i| { - let val = val.clone(); + // let caller = { + // let it = (0..10) + // .map(|i| { + // let val = val.clone(); - LazyNodes::new_some(move |f| { - log::debug!("hell closure"); - let inner = DropInner { id: i }; - f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) - }) - }) - .collect::>(); + // LazyNodes::new_some(move |f| { + // log::debug!("hell closure"); + // let inner = DropInner { id: i }; + // f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) + // }) + // }) + // .collect::>(); - LazyNodes::new_some(|f| { - log::debug!("main closure"); - f.fragment_from_iter(it) - }) - }; + // LazyNodes::new_some(|f| { + // log::debug!("main closure"); + // f.fragment_from_iter(it) + // }) + // }; - let _ = caller.call(factory); - assert_eq!(Rc::strong_count(&val), 1); + // let _ = caller.call(factory); + // assert_eq!(Rc::strong_count(&val), 1); } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 0acb08829..18a5c32ad 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -395,12 +395,16 @@ impl

AnyProps for VComponentProps

{ /// This struct adds metadata to the final VNode about listeners, attributes, and children #[derive(Copy, Clone)] pub struct NodeFactory<'a> { + pub(crate) scope: &'a ScopeState, pub(crate) bump: &'a Bump, } impl<'a> NodeFactory<'a> { - pub fn new(bump: &'a Bump) -> NodeFactory<'a> { - NodeFactory { bump } + pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> { + NodeFactory { + scope, + bump: &scope.wip_frame().bump, + } } #[inline] @@ -473,6 +477,12 @@ impl<'a> NodeFactory<'a> { ) -> VNode<'a> { let key = key.map(|f| self.raw_text(f).0); + let mut items = self.scope.items.borrow_mut(); + for listener in listeners { + let long_listener = unsafe { std::mem::transmute(listener) }; + items.listeners.push(long_listener); + } + VNode::Element(self.bump.alloc(VElement { tag: tag_name, key, @@ -511,7 +521,7 @@ impl<'a> NodeFactory<'a> { where P: Properties + 'a, { - VNode::Component(self.bump.alloc(VComponent { + let vcomp = self.bump.alloc(VComponent { key: key.map(|f| self.raw_text(f).0), scope: Default::default(), can_memoize: P::IS_STATIC, @@ -528,7 +538,15 @@ impl<'a> NodeFactory<'a> { // the transformation from this specific lifetime to the for<'a> lifetime render_fn: unsafe { std::mem::transmute(component) }, }))), - })) + }); + + if !P::IS_STATIC { + let vcomp = &*vcomp; + let vcomp = unsafe { std::mem::transmute(vcomp) }; + self.scope.items.borrow_mut().borrowed_props.push(vcomp); + } + + VNode::Component(vcomp) } pub fn listener(self, event: &'static str, callback: EventHandler<'a>) -> Listener<'a> { @@ -730,8 +748,11 @@ impl IntoVNode<'_> for Arguments<'_> { impl<'a> IntoVNode<'a> for &Option> { fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { - let r = self.as_ref().map(|f| f.decouple()); - cx.fragment_from_iter(r) + self.as_ref() + .map(|f| f.decouple()) + .unwrap_or(VNode::Placeholder( + cx.bump.alloc(VPlaceholder { id: empty_cell() }), + )) } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 8f35a3ec0..a7ee64299 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -143,6 +143,7 @@ impl ScopeArena { // Removes a scope and its descendents from the arena pub fn try_remove(&self, id: ScopeId) -> Option<()> { + log::debug!("removing scope {:?}", id); self.ensure_drop_safety(id); // Safety: @@ -188,6 +189,8 @@ impl ScopeArena { /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will /// be dropped. pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { + log::debug!("Ensuring drop safety for scope {:?}", scope_id); + if let Some(scope) = self.get_scope(scope_id) { let mut items = scope.items.borrow_mut(); @@ -217,6 +220,7 @@ impl ScopeArena { // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners + log::debug!("Running scope {:?}", id); self.ensure_drop_safety(id); // todo: we *know* that this is aliased by the contents of the scope itself @@ -278,14 +282,19 @@ impl ScopeArena { let nodes = self.nodes.borrow(); let mut cur_el = Some(element); + log::debug!("calling listener {:?}, {:?}", event, element); let state = Rc::new(BubbleState::new()); while let Some(id) = cur_el.take() { if let Some(el) = nodes.get(id.0) { + log::debug!("Found valid receiver element"); + let real_el = unsafe { &**el }; if let VNode::Element(real_el) = real_el { for listener in real_el.listeners.borrow().iter() { if listener.event == event.name { + log::debug!("Found valid receiver event"); + if state.canceled.get() { // stop bubbling if canceled break; @@ -651,12 +660,14 @@ impl ScopeState { /// rsx!(cx, div { "hello {state.0}" }) /// } /// ``` - pub fn provide_context(&self, value: T) { + pub fn provide_context(&self, value: T) -> Rc { + let value = Rc::new(value); self.shared_contexts .borrow_mut() - .insert(TypeId::of::(), Rc::new(value)) + .insert(TypeId::of::(), value.clone()) .map(|f| f.downcast::().ok()) .flatten(); + value } /// Try to retrieve a SharedState with type T from the any parent Scope. @@ -713,6 +724,7 @@ impl ScopeState { ///``` pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option> { Some(rsx.call(NodeFactory { + scope: self, bump: &self.wip_frame().bump, })) } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3cbb3fe5d..29e8355cc 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -531,6 +531,9 @@ impl VirtualDom { diff_state.stack.element_stack.push(ElementId(0)); diff_state.stack.scope_stack.push(scope_id); diff_state.work(|| false); + self.dirty_scopes.clear(); + assert!(self.dirty_scopes.is_empty()); + diff_state.mutations } @@ -599,7 +602,7 @@ impl VirtualDom { pub fn render_vnodes<'a>(&'a self, lazy_nodes: LazyNodes<'a, '_>) -> &'a VNode<'a> { let scope = self.scopes.get_scope(ScopeId(0)).unwrap(); let frame = scope.wip_frame(); - let factory = NodeFactory { bump: &frame.bump }; + let factory = NodeFactory::new(scope); let node = lazy_nodes.call(factory); frame.bump.alloc(node) } diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 2d381fddb..bb4963b71 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -97,7 +97,7 @@ fn memo_works_properly() { cx.render(rsx!( div { "Hello, world! {name}" } - child(na: "asdfg".to_string()) + child(na: "asdfg".to_string() ) )) } @@ -285,3 +285,31 @@ fn basic() { dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); dom.work_with_deadline(|| false); } + +#[test] +fn leak_thru_children() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + Child { + name: "asd".to_string(), + } + }); + cx.render(rsx! { + div {} + }) + } + + #[inline_props] + fn Child(cx: Scope, name: String) -> Element { + rsx!(cx, div { "child {name}" }) + } + + let mut dom = new_dom(app, ()); + let _ = dom.rebuild(); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); +} diff --git a/packages/hooks/src/usestate/owned.rs b/packages/hooks/src/usestate/owned.rs index c4cfef112..a1a6bdb73 100644 --- a/packages/hooks/src/usestate/owned.rs +++ b/packages/hooks/src/usestate/owned.rs @@ -12,8 +12,8 @@ pub struct UseStateOwned { } impl UseStateOwned { - pub fn get(&self) -> Ref { - Ref::map(self.wip.borrow(), |x| x.as_ref().unwrap()) + pub fn get(&self) -> Ref> { + self.wip.borrow() } pub fn set(&self, new_val: T) { @@ -21,8 +21,9 @@ impl UseStateOwned { (self.update_callback)(); } - pub fn modify(&self) -> RefMut { - RefMut::map(self.wip.borrow_mut(), |x| x.as_mut().unwrap()) + pub fn modify(&self) -> RefMut> { + (self.update_callback)(); + self.wip.borrow_mut() } } diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 02b0046ad..1a2f36e57 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1100,6 +1100,11 @@ impl label { cx.attr("for", val, None, false) } } +impl a { + pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr("dioxus-prevent-default", val, None, false) + } +} builder_constructors! { // SVG components diff --git a/packages/router/.vscode/settings.json b/packages/router/.vscode/settings.json new file mode 100644 index 000000000..e0975d874 --- /dev/null +++ b/packages/router/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.allFeatures": true +} diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 158a233fa..a182822ab 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -19,6 +19,7 @@ serde = "1" url = "2.2.2" serde_urlencoded = "0.7" +# for wasm web-sys = { version = "0.3", features = [ "Attr", "Document", @@ -33,11 +34,12 @@ web-sys = { version = "0.3", features = [ wasm-bindgen = { version = "0.2", optional = true } js-sys = { version = "0.3", optional = true } gloo = { version = "0.5", optional = true } +log = "0.4.14" [features] -default = ["derive"] -web = ["web-sys"] +default = ["derive", "web"] +web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] desktop = [] mobile = [] derive = [] diff --git a/packages/router/README.md b/packages/router/README.md index 4cfa98c9d..c32441888 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -1,40 +1,29 @@ -# Router hook for Dioxus apps +# Routing for Dioxus App -Dioxus-router provides a use_router hook that returns a different value depending on the route. -The router is generic over any value, however it makes sense to return a different set of VNodes -and feed them into the App's return VNodes. - -Using the router should feel similar to tide's routing framework where an "address" book is assembled at the head of the app. - -Here's an example of how to use the router hook: +DioxusRouter adds React-Router style routing to your Dioxus apps. Works in browser, SSR, and natively. ```rust -#[derive(Clone, PartialEq, Serialize, Deserialize, Routable)] -enum AppRoute { - Home, - Posts, - NotFound -} - -static App: Component = |cx| { - let route = use_router(cx, AppRoute::parse); - - match route { - AppRoute::Home => rsx!(cx, Home {}) - AppRoute::Posts => rsx!(cx, Posts {}) - AppRoute::Notfound => rsx!(cx, Notfound {}) - } -}; -``` - -Adding links into your app: - -```rust -static Leaf: Component = |cx| { - rsx!(cx, div { - Link { to: AppRoute::Home } +fn app() { + cx.render(rsx! { + Routes { + Route { to: "/", Component {} }, + Route { to: "/blog", Blog {} }, + Route { to: "/blog/:id", BlogPost {} }, + } }) } ``` +Then, in your route, you can choose to parse the Route any way you want through `use_route`. +```rust +let id: usize = use_route(&cx).path("id")?; + +let state: CustomState = use_route(&cx).parse()?; +``` + +Adding links into your app: +```rust +Link { to: "id/{id}" } +``` + Currently, the router is only supported in a web environment, but we plan to add 1st-party support via the context API when new renderers are available. diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple.rs index 134237dbb..4aebafb84 100644 --- a/packages/router/examples/simple.rs +++ b/packages/router/examples/simple.rs @@ -11,31 +11,40 @@ fn main() { } static APP: Component = |cx| { - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] - enum Route { - Home, - About, - NotFound, - } - impl Default for Route { - fn default() -> Self { - Route::Home - } - } - - let route = use_router(&cx, |c| {}); - cx.render(rsx! { - div { - {match route { - Route::Home => rsx!(h1 { "Home" }), - Route::About => rsx!(h1 { "About" }), - Route::NotFound => rsx!(h1 { "NotFound" }), - }} - nav { - Link { to: Route::Home, href: "/" } - Link { to: Route::About, href: "/about" } + Router { + onchange: move |route| log::info!("route changed to {}", route), + Route { to: "/", Home {} } + Route { to: "blog" + Route { to: "/", BlogList {} } + Route { to: ":id", BlogPost {} } } } }) }; + +fn Home(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Home" } + } + }) +} + +fn BlogList(cx: Scope) -> Element { + cx.render(rsx! { + div { + + } + }) +} + +fn BlogPost(cx: Scope) -> Element { + let id = use_route(&cx).segment::("id")?; + + cx.render(rsx! { + div { + + } + }) +} diff --git a/packages/router/src/link.rs b/packages/router/src/components/link.rs similarity index 62% rename from packages/router/src/link.rs rename to packages/router/src/components/link.rs index 4ad7a45d0..66fa5a7f8 100644 --- a/packages/router/src/link.rs +++ b/packages/router/src/components/link.rs @@ -1,4 +1,4 @@ -use crate::{Routable, RouterService}; +use crate::RouterService; use dioxus::Attribute; use dioxus_core as dioxus; use dioxus_core::prelude::*; @@ -6,8 +6,8 @@ use dioxus_core_macro::{format_args_f, rsx, Props}; use dioxus_html as dioxus_elements; #[derive(Props)] -pub struct LinkProps<'a, R: Routable> { - to: R, +pub struct LinkProps<'a> { + to: &'a str, /// The url that gets pushed to the history stack /// @@ -28,6 +28,9 @@ pub struct LinkProps<'a, R: Routable> { #[props(default, setter(strip_option))] class: Option<&'a str>, + #[props(default, setter(strip_option))] + id: Option<&'a str>, + children: Element<'a>, #[props(default)] @@ -35,13 +38,21 @@ pub struct LinkProps<'a, R: Routable> { } #[allow(non_snake_case)] -pub fn Link<'a, R: Routable>(cx: Scope<'a, LinkProps<'a, R>>) -> Element { - let service = cx.consume_context::>()?; +pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { + log::debug!("rendering link {:?}", cx.scope_id()); + let service = cx.consume_context::()?; cx.render(rsx! { a { - href: "#", + href: "{cx.props.to}", class: format_args!("{}", cx.props.class.unwrap_or("")), - onclick: move |_| service.push_route(cx.props.to.clone()), + id: format_args!("{}", cx.props.id.unwrap_or("")), + + prevent_default: "onclick", + onclick: move |_| { + log::debug!("clicked"); + service.push_route(cx.props.to.clone()); + }, + &cx.props.children } }) diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs new file mode 100644 index 000000000..99b25a0b2 --- /dev/null +++ b/packages/router/src/components/route.rs @@ -0,0 +1,58 @@ +use dioxus_core::Element; + +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_core_macro::Props; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +use crate::{RouteContext, RouterService}; + +#[derive(Props)] +pub struct RouteProps<'a> { + to: &'a str, + children: Element<'a>, +} + +pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { + // now we want to submit + let router_root = cx + .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) => format!("{}", ctx.total_route.clone()), + None => format!("{}", cx.props.to.clone()), + }; + + // provide our route context + let route_context = cx.provide_context(RouteContext { + declared_route: cx.props.to.to_string(), + total_route, + }); + + // submit our rout + router_root.register_total_route(route_context.total_route.clone(), cx.scope_id()); + + Some(RouteInner {}) + }); + + log::debug!("rendering route {:?}", cx.scope_id()); + + if router_root.should_render(cx.scope_id()) { + log::debug!("route {:?} produced nodes", cx.scope_id()); + cx.render(rsx!(&cx.props.children)) + } else { + 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 new file mode 100644 index 000000000..416b93722 --- /dev/null +++ b/packages/router/src/components/router.rs @@ -0,0 +1,29 @@ +use dioxus_core::Element; + +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +use crate::RouterService; + +#[derive(Props)] +pub struct RouterProps<'a> { + children: Element<'a>, + + #[props(default, setter(strip_option))] + onchange: Option<&'a Fn(&'a str)>, +} + +pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { + let p = cx.use_hook(|_| { + let update = cx.schedule_update_any(); + cx.provide_context(RouterService::new(update, cx.scope_id())) + }); + + log::debug!("rendering router {:?}", cx.scope_id()); + + cx.render(rsx!( + div { &cx.props.children } + )) +} diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs new file mode 100644 index 000000000..66b0b4f11 --- /dev/null +++ b/packages/router/src/hooks/use_route.rs @@ -0,0 +1,30 @@ +use dioxus_core::ScopeState; + +pub struct UseRoute<'a> { + cur_route: String, + cx: &'a ScopeState, +} + +impl<'a> UseRoute<'a> { + /// Parse the query part of the URL + pub fn param(&self, param: &str) -> Option<&T> { + todo!() + } + + pub fn nth_segment(&self, n: usize) -> Option<&str> { + todo!() + } + + pub fn last_segment(&self) -> Option<&str> { + todo!() + } + + /// Parse the segments of the URL, using named parameters (defined in your router) + pub fn segment(&self, name: &str) -> Option<&T> { + todo!() + } +} + +pub fn use_route(cx: &ScopeState) -> UseRoute { + todo!() +} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index da7feb05e..160cd9861 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -7,17 +7,6 @@ //! //! ```rust //! fn app(cx: Scope) -> Element { -//! let route = use_router(&cx, |svc, path| { -//! match path { -//! "/about" => Route::About, -//! _ => Route::Home, -//! } -//! }); -//! -//! match route { -//! Route::Home => rsx!(cx, h1 { "Home" }), -//! Route::About => rsx!(cx, h1 { "About" }), -//! } //! } //! //! @@ -35,22 +24,28 @@ //! //! -mod link; +mod hooks { + mod use_route; + pub use use_route::*; +} +pub use hooks::*; + +mod components { + mod router; + pub use router::*; + + mod route; + pub use route::*; + + mod link; + pub use link::*; +} +pub use components::*; + mod platform; +mod routecontext; mod service; -mod userouter; mod utils; -pub use link::*; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +pub use routecontext::*; pub use service::*; -pub use userouter::*; - -pub trait Routable: - 'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default -{ -} -impl Routable for T where - T: 'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default -{ -} diff --git a/packages/router/src/platform.rs b/packages/router/src/platform.rs deleted file mode 100644 index 55cdceb83..000000000 --- a/packages/router/src/platform.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub fn parse_current_route() -> String { - todo!() -} - -pub(crate) fn get_base_route() -> String { - todo!() -} diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs new file mode 100644 index 000000000..bbb0e7c18 --- /dev/null +++ b/packages/router/src/platform/mod.rs @@ -0,0 +1,11 @@ +use url::Url; + +pub trait RouterProvider { + fn get_current_route(&self) -> String; + fn subscribe_to_route_changes(&self, callback: Box); +} + +enum RouteChange { + LinkTo(String), + Back(String), +} diff --git a/packages/router/src/routecontext.rs b/packages/router/src/routecontext.rs new file mode 100644 index 000000000..fdd4f9d90 --- /dev/null +++ b/packages/router/src/routecontext.rs @@ -0,0 +1,7 @@ +pub struct RouteContext { + // "/name/:id" + pub declared_route: String, + + // "app/name/:id" + pub total_route: String, +} diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index e70aaf410..56a57abdb 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,18 +1,75 @@ -use crate::Routable; -use std::{cell::RefCell, rc::Rc}; +use gloo::history::{BrowserHistory, History, HistoryListener}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; -pub struct RouterService { - pub(crate) regen_route: Rc, - pub(crate) pending_routes: RefCell>, +use dioxus_core::ScopeId; + +pub struct RouterService { + pub(crate) regen_route: Rc, + history: RefCell, + registerd_routes: RefCell, + slots: RefCell>, + cur_root: RefCell, + listener: HistoryListener, } -impl RouterService { - pub fn current_path(&self) -> &str { - todo!() +enum RouteSlot { + Routes { + // the partial route + partial: String, + + // the total route + total: String, + + // Connections to other routs + rest: Vec, + }, +} + +impl RouterService { + pub fn new(regen_route: Rc, root_scope: ScopeId) -> Self { + let history = BrowserHistory::default(); + let location = history.location(); + let path = location.path(); + + let regen = Rc::clone(®en_route); + let listener = history.listen(move || { + regen(root_scope); + }); + + Self { + registerd_routes: RefCell::new(RouteSlot::Routes { + partial: String::from("/"), + total: String::from("/"), + rest: Vec::new(), + }), + history: RefCell::new(history), + regen_route, + slots: Default::default(), + cur_root: RefCell::new(path.to_string()), + listener, + } } - pub fn push_route(&self, route: R) { - self.pending_routes.borrow_mut().push(route); - (self.regen_route)(); + + pub fn push_route(&self, route: &str) { + self.history.borrow_mut().push(route); + } + + pub fn register_total_route(&self, route: String, scope: ScopeId) { + self.slots.borrow_mut().insert(scope, route); + } + + pub fn should_render(&self, scope: ScopeId) -> bool { + let location = self.history.borrow().location(); + let path = location.path(); + + let roots = self.slots.borrow(); + + let root = roots.get(&scope); + + match root { + Some(r) => r == path, + None => false, + } } } diff --git a/packages/router/src/userouter.rs b/packages/router/src/userouter.rs deleted file mode 100644 index 4736b634e..000000000 --- a/packages/router/src/userouter.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use crate::{Routable, RouterCfg, RouterService}; -use dioxus_core::ScopeState; - -/// Initialize the app's router service and provide access to `Link` components -pub fn use_router<'a, R: Routable>(cx: &'a ScopeState, _cfg: impl FnOnce(&mut RouterCfg)) -> &'a R { - let router = cx.use_hook(|_| { - let svc: RouterService = RouterService { - regen_route: cx.schedule_update(), - pending_routes: RefCell::new(Vec::new()), - }; - let first_path = R::default(); - cx.provide_context(svc); - UseRouterInner { - svc: cx.consume_context::>().unwrap(), - history: vec![first_path], - } - }); - - let mut pending_routes = router.svc.pending_routes.borrow_mut(); - - for route in pending_routes.drain(..) { - router.history.push(route); - } - - router.history.last().unwrap() -} - -struct UseRouterInner { - svc: Rc>, - history: Vec, -} diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 655b6c8c5..b93be0f63 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -274,7 +274,21 @@ impl WebsysDom { // "Result" cannot be received from JS // Instead, we just build and immediately execute a closure that returns result match decode_trigger(event) { - Ok(synthetic_event) => trigger.as_ref()(SchedulerMsg::Event(synthetic_event)), + Ok(synthetic_event) => { + let target = event.target().unwrap(); + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::debug!("Preventing default"); + event.prevent_default(); + } + } + } + + trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) + } Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), }; }); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ac07f7e71..61caa99b9 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -157,6 +157,7 @@ pub async fn run_with_props(root: Component, root_props: T let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback); + log::debug!("rebuilding app"); let mut mutations = dom.rebuild(); // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding @@ -168,10 +169,13 @@ pub async fn run_with_props(root: Component, root_props: T let work_loop = ric_raf::RafLoop::new(); loop { + log::debug!("waiting for work"); // if virtualdom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately. dom.wait_for_work().await; + log::debug!("working.."); + // wait for the mainthread to schedule us in let mut deadline = work_loop.wait_for_idle_time().await;