wip: memoize dom in the prescence of identical components

This commit is contained in:
Jonathan Kelley committed 2022-01-05 00:27:22 -05:00
1 parent 7cf15ee4e8
commit cb2782b4bb
27 files changed
+474 -219

No files matched your search

+2 -2
View File
@@ -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-desktop = { path = "./packages/desktop", version = "^0.1.3", optional = true }
dioxus-ssr = { path = "./packages/ssr", version = "0.1.1", 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-mobile = { path = "./packages/mobile", version = "^0.0.2", optional = true }
# dioxus-liveview = { path = "./packages/liveview", optional = true } # dioxus-liveview = { path = "./packages/liveview", optional = true }
@@ -33,7 +33,7 @@ html = ["dioxus-html"]
ssr = ["dioxus-ssr"] ssr = ["dioxus-ssr"]
web = ["dioxus-web"] web = ["dioxus-web"]
desktop = ["dioxus-desktop"] desktop = ["dioxus-desktop"]
# router = ["dioxus-router"] router = ["dioxus-router"]
# "dioxus-router/web" # "dioxus-router/web"
# "dioxus-router/desktop" # "dioxus-router/desktop"
+20 -10
View File
@@ -19,7 +19,7 @@ use quote::{quote, ToTokens, TokenStreamExt};
use syn::{ use syn::{
ext::IdentExt, ext::IdentExt,
parse::{Parse, ParseBuffer, ParseStream}, parse::{Parse, ParseBuffer, ParseStream},
token, Expr, Ident, Result, Token, token, Expr, Ident, LitStr, Result, Token,
}; };
pub struct Component { pub struct Component {
@@ -135,8 +135,6 @@ impl ToTokens for Component {
None => quote! { None }, None => quote! { None },
}; };
// #children
tokens.append_all(quote! { tokens.append_all(quote! {
__cx.component( __cx.component(
#name, #name,
@@ -155,7 +153,7 @@ pub struct ComponentField {
enum ContentField { enum ContentField {
ManExpr(Expr), ManExpr(Expr),
Formatted(LitStr),
OnHandlerRaw(Expr), OnHandlerRaw(Expr),
} }
@@ -163,6 +161,9 @@ impl ToTokens for ContentField {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
match self { match self {
ContentField::ManExpr(e) => e.to_tokens(tokens), 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! { ContentField::OnHandlerRaw(e) => tokens.append_all(quote! {
__cx.bump().alloc(#e) __cx.bump().alloc(#e)
}), }),
@@ -175,13 +176,22 @@ impl Parse for ComponentField {
let name = Ident::parse_any(input)?; let name = Ident::parse_any(input)?;
input.parse::<Token![:]>()?; input.parse::<Token![:]>()?;
let name_str = name.to_string(); if name.to_string().starts_with("on") {
let content = if name_str.starts_with("on") { let content = ContentField::OnHandlerRaw(input.parse()?);
ContentField::OnHandlerRaw(input.parse()?) return Ok(Self { name, content });
} else { }
ContentField::ManExpr(input.parse::<Expr>()?)
};
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 }) Ok(Self { name, content })
} }
} }
+1 -1
View File
@@ -46,7 +46,7 @@ impl Parse for Element {
content.parse::<Token![:]>()?; content.parse::<Token![:]>()?;
if content.peek(LitStr) { if content.peek(LitStr) && content.peek2(Token![,]) {
let value = content.parse::<LitStr>()?; let value = content.parse::<LitStr>()?;
attributes.push(ElementAttrNamed { attributes.push(ElementAttrNamed {
el_name: el_name.clone(), el_name: el_name.clone(),
+12 -16
View File
@@ -390,10 +390,7 @@ impl<'bump> DiffState<'bump> {
self.stack.add_child_count(1); self.stack.add_child_count(1);
if let Some(cur_scope_id) = self.stack.current_scope() { if let Some(cur_scope_id) = self.stack.current_scope() {
let scope = self.scopes.get_scope(cur_scope_id).unwrap();
for listener in *listeners { for listener in *listeners {
self.attach_listener_to_scope(listener, scope);
listener.mounted_node.set(Some(real_id)); listener.mounted_node.set(Some(real_id));
self.mutations.new_event_listener(listener, cur_scope_id); self.mutations.new_event_listener(listener, cur_scope_id);
} }
@@ -446,9 +443,9 @@ impl<'bump> DiffState<'bump> {
} }
false => { false => {
// track this component internally so we know the right drop order // track this component internally so we know the right drop order
let cur_scope = self.scopes.get_scope(parent_idx).unwrap(); // let cur_scope = self.scopes.get_scope(parent_idx).unwrap();
let extended = unsafe { std::mem::transmute(vcomponent) }; // let extended = unsafe { std::mem::transmute(vcomponent) };
cur_scope.items.borrow_mut().borrowed_props.push(extended); // 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 // TODO: take a more efficient path than this
if let Some(cur_scope_id) = self.stack.current_scope() { 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() { if old.listeners.len() == new.listeners.len() {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
if old_l.event != new_l.event { 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); self.mutations.new_event_listener(new_l, cur_scope_id);
} }
new_l.mounted_node.set(old_l.mounted_node.get()); new_l.mounted_node.set(old_l.mounted_node.get());
self.attach_listener_to_scope(new_l, scope);
} }
} else { } else {
for listener in old.listeners { for listener in old.listeners {
@@ -600,7 +594,6 @@ impl<'bump> DiffState<'bump> {
for listener in new.listeners { for listener in new.listeners {
listener.mounted_node.set(Some(root)); listener.mounted_node.set(Some(root));
self.mutations.new_event_listener(listener, cur_scope_id); 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>, new: &'bump VComponent<'bump>,
) { ) {
let scope_addr = old.scope.get().unwrap(); 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) // Make sure we're dealing with the same component (by function pointer)
if old.user_fc == new.user_fc { if old.user_fc == new.user_fc {
@@ -728,11 +727,14 @@ impl<'bump> DiffState<'bump> {
// this should auto drop the previous props // this should auto drop the previous props
self.scopes.run_scope(scope_addr); self.scopes.run_scope(scope_addr);
self.mutations.dirty_scopes.insert(scope_addr);
self.diff_node( self.diff_node(
self.scopes.wip_head(scope_addr), self.scopes.wip_head(scope_addr),
self.scopes.fin_head(scope_addr), self.scopes.fin_head(scope_addr),
); );
} else { } else {
log::debug!("memoized");
// memoization has taken place // memoization has taken place
drop(new_props); 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)
}
} }
+35 -35
View File
@@ -244,15 +244,15 @@ fn round_to_words(len: usize) -> usize {
fn it_works() { fn it_works() {
let bump = bumpalo::Bump::new(); let bump = bumpalo::Bump::new();
let factory = NodeFactory { bump: &bump }; // let factory = NodeFactory { bump: &bump };
let caller = LazyNodes::new_some(|f| { // let caller = LazyNodes::new_some(|f| {
// // //
f.text(format_args!("hello world!")) // f.text(format_args!("hello world!"))
}); // });
let g = caller.call(factory); // let g = caller.call(factory);
dbg!(g); // dbg!(g);
} }
#[test] #[test]
@@ -260,37 +260,37 @@ fn it_drops() {
use std::rc::Rc; use std::rc::Rc;
let bump = bumpalo::Bump::new(); let bump = bumpalo::Bump::new();
let factory = NodeFactory { bump: &bump }; // let factory = NodeFactory { bump: &bump };
struct DropInner { // struct DropInner {
id: i32, // id: i32,
} // }
impl Drop for DropInner { // impl Drop for DropInner {
fn drop(&mut self) { // fn drop(&mut self) {
log::debug!("dropping inner"); // log::debug!("dropping inner");
} // }
} // }
let val = Rc::new(10); // let val = Rc::new(10);
let caller = { // let caller = {
let it = (0..10) // let it = (0..10)
.map(|i| { // .map(|i| {
let val = val.clone(); // let val = val.clone();
LazyNodes::new_some(move |f| { // LazyNodes::new_some(move |f| {
log::debug!("hell closure"); // log::debug!("hell closure");
let inner = DropInner { id: i }; // let inner = DropInner { id: i };
f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) // f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
}) // })
}) // })
.collect::<Vec<_>>(); // .collect::<Vec<_>>();
LazyNodes::new_some(|f| { // LazyNodes::new_some(|f| {
log::debug!("main closure"); // log::debug!("main closure");
f.fragment_from_iter(it) // f.fragment_from_iter(it)
}) // })
}; // };
let _ = caller.call(factory); // let _ = caller.call(factory);
assert_eq!(Rc::strong_count(&val), 1); // assert_eq!(Rc::strong_count(&val), 1);
} }
+27 -6
View File
@@ -395,12 +395,16 @@ impl<P> AnyProps for VComponentProps<P> {
/// This struct adds metadata to the final VNode about listeners, attributes, and children /// This struct adds metadata to the final VNode about listeners, attributes, and children
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct NodeFactory<'a> { pub struct NodeFactory<'a> {
pub(crate) scope: &'a ScopeState,
pub(crate) bump: &'a Bump, pub(crate) bump: &'a Bump,
} }
impl<'a> NodeFactory<'a> { impl<'a> NodeFactory<'a> {
pub fn new(bump: &'a Bump) -> NodeFactory<'a> { pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> {
NodeFactory { bump } NodeFactory {
scope,
bump: &scope.wip_frame().bump,
}
} }
#[inline] #[inline]
@@ -473,6 +477,12 @@ impl<'a> NodeFactory<'a> {
) -> VNode<'a> { ) -> VNode<'a> {
let key = key.map(|f| self.raw_text(f).0); 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 { VNode::Element(self.bump.alloc(VElement {
tag: tag_name, tag: tag_name,
key, key,
@@ -511,7 +521,7 @@ impl<'a> NodeFactory<'a> {
where where
P: Properties + 'a, P: Properties + 'a,
{ {
VNode::Component(self.bump.alloc(VComponent { let vcomp = self.bump.alloc(VComponent {
key: key.map(|f| self.raw_text(f).0), key: key.map(|f| self.raw_text(f).0),
scope: Default::default(), scope: Default::default(),
can_memoize: P::IS_STATIC, can_memoize: P::IS_STATIC,
@@ -528,7 +538,15 @@ impl<'a> NodeFactory<'a> {
// the transformation from this specific lifetime to the for<'a> lifetime // the transformation from this specific lifetime to the for<'a> lifetime
render_fn: unsafe { std::mem::transmute(component) }, 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> { 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<VNode<'a>> { impl<'a> IntoVNode<'a> for &Option<VNode<'a>> {
fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> {
let r = self.as_ref().map(|f| f.decouple()); self.as_ref()
cx.fragment_from_iter(r) .map(|f| f.decouple())
.unwrap_or(VNode::Placeholder(
cx.bump.alloc(VPlaceholder { id: empty_cell() }),
))
} }
} }
+14 -2
View File
@@ -143,6 +143,7 @@ impl ScopeArena {
// Removes a scope and its descendents from the arena // Removes a scope and its descendents from the arena
pub fn try_remove(&self, id: ScopeId) -> Option<()> { pub fn try_remove(&self, id: ScopeId) -> Option<()> {
log::debug!("removing scope {:?}", id);
self.ensure_drop_safety(id); self.ensure_drop_safety(id);
// Safety: // 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 /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will
/// be dropped. /// be dropped.
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { 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) { if let Some(scope) = self.get_scope(scope_id) {
let mut items = scope.items.borrow_mut(); let mut items = scope.items.borrow_mut();
@@ -217,6 +220,7 @@ impl ScopeArena {
// Cycle to the next frame and then reset it // Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it. // This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners // Remove all the outdated listeners
log::debug!("Running scope {:?}", id);
self.ensure_drop_safety(id); self.ensure_drop_safety(id);
// todo: we *know* that this is aliased by the contents of the scope itself // 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 nodes = self.nodes.borrow();
let mut cur_el = Some(element); let mut cur_el = Some(element);
log::debug!("calling listener {:?}, {:?}", event, element);
let state = Rc::new(BubbleState::new()); let state = Rc::new(BubbleState::new());
while let Some(id) = cur_el.take() { while let Some(id) = cur_el.take() {
if let Some(el) = nodes.get(id.0) { if let Some(el) = nodes.get(id.0) {
log::debug!("Found valid receiver element");
let real_el = unsafe { &**el }; let real_el = unsafe { &**el };
if let VNode::Element(real_el) = real_el { if let VNode::Element(real_el) = real_el {
for listener in real_el.listeners.borrow().iter() { for listener in real_el.listeners.borrow().iter() {
if listener.event == event.name { if listener.event == event.name {
log::debug!("Found valid receiver event");
if state.canceled.get() { if state.canceled.get() {
// stop bubbling if canceled // stop bubbling if canceled
break; break;
@@ -651,12 +660,14 @@ impl ScopeState {
/// rsx!(cx, div { "hello {state.0}" }) /// rsx!(cx, div { "hello {state.0}" })
/// } /// }
/// ``` /// ```
pub fn provide_context<T: 'static>(&self, value: T) { pub fn provide_context<T: 'static>(&self, value: T) -> Rc<T> {
let value = Rc::new(value);
self.shared_contexts self.shared_contexts
.borrow_mut() .borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(value)) .insert(TypeId::of::<T>(), value.clone())
.map(|f| f.downcast::<T>().ok()) .map(|f| f.downcast::<T>().ok())
.flatten(); .flatten();
value
} }
/// Try to retrieve a SharedState with type T from the any parent Scope. /// 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<VNode<'src>> { pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option<VNode<'src>> {
Some(rsx.call(NodeFactory { Some(rsx.call(NodeFactory {
scope: self,
bump: &self.wip_frame().bump, bump: &self.wip_frame().bump,
})) }))
} }
+4 -1
View File
@@ -531,6 +531,9 @@ impl VirtualDom {
diff_state.stack.element_stack.push(ElementId(0)); diff_state.stack.element_stack.push(ElementId(0));
diff_state.stack.scope_stack.push(scope_id); diff_state.stack.scope_stack.push(scope_id);
diff_state.work(|| false); diff_state.work(|| false);
self.dirty_scopes.clear();
assert!(self.dirty_scopes.is_empty());
diff_state.mutations diff_state.mutations
} }
@@ -599,7 +602,7 @@ impl VirtualDom {
pub fn render_vnodes<'a>(&'a self, lazy_nodes: LazyNodes<'a, '_>) -> &'a VNode<'a> { pub fn render_vnodes<'a>(&'a self, lazy_nodes: LazyNodes<'a, '_>) -> &'a VNode<'a> {
let scope = self.scopes.get_scope(ScopeId(0)).unwrap(); let scope = self.scopes.get_scope(ScopeId(0)).unwrap();
let frame = scope.wip_frame(); let frame = scope.wip_frame();
let factory = NodeFactory { bump: &frame.bump }; let factory = NodeFactory::new(scope);
let node = lazy_nodes.call(factory); let node = lazy_nodes.call(factory);
frame.bump.alloc(node) frame.bump.alloc(node)
} }
+29 -1
View File
@@ -97,7 +97,7 @@ fn memo_works_properly() {
cx.render(rsx!( cx.render(rsx!(
div { "Hello, world! {name}" } 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.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false); 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);
}
+5 -4
View File
@@ -12,8 +12,8 @@ pub struct UseStateOwned<T: 'static> {
} }
impl<T> UseStateOwned<T> { impl<T> UseStateOwned<T> {
pub fn get(&self) -> Ref<T> { pub fn get(&self) -> Ref<Option<T>> {
Ref::map(self.wip.borrow(), |x| x.as_ref().unwrap()) self.wip.borrow()
} }
pub fn set(&self, new_val: T) { pub fn set(&self, new_val: T) {
@@ -21,8 +21,9 @@ impl<T> UseStateOwned<T> {
(self.update_callback)(); (self.update_callback)();
} }
pub fn modify(&self) -> RefMut<T> { pub fn modify(&self) -> RefMut<Option<T>> {
RefMut::map(self.wip.borrow_mut(), |x| x.as_mut().unwrap()) (self.update_callback)();
self.wip.borrow_mut()
} }
} }
+5
View File
@@ -1100,6 +1100,11 @@ impl label {
cx.attr("for", val, None, false) 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! { builder_constructors! {
// SVG components // SVG components
+3
View File
@@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.allFeatures": true
}
+4 -2
View File
@@ -19,6 +19,7 @@ serde = "1"
url = "2.2.2" url = "2.2.2"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
# for wasm
web-sys = { version = "0.3", features = [ web-sys = { version = "0.3", features = [
"Attr", "Attr",
"Document", "Document",
@@ -33,11 +34,12 @@ web-sys = { version = "0.3", features = [
wasm-bindgen = { version = "0.2", optional = true } wasm-bindgen = { version = "0.2", optional = true }
js-sys = { version = "0.3", optional = true } js-sys = { version = "0.3", optional = true }
gloo = { version = "0.5", optional = true } gloo = { version = "0.5", optional = true }
log = "0.4.14"
[features] [features]
default = ["derive"] default = ["derive", "web"]
web = ["web-sys"] web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"]
desktop = [] desktop = []
mobile = [] mobile = []
derive = [] derive = []
+21 -32
View File
@@ -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. DioxusRouter adds React-Router style routing to your Dioxus apps. Works in browser, SSR, and natively.
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:
```rust ```rust
#[derive(Clone, PartialEq, Serialize, Deserialize, Routable)] fn app() {
enum AppRoute { cx.render(rsx! {
Home, Routes {
Posts, Route { to: "/", Component {} },
NotFound Route { to: "/blog", Blog {} },
} Route { to: "/blog/:id", BlogPost {} },
}
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 }
}) })
} }
``` ```
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. 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.
+32 -23
View File
@@ -11,31 +11,40 @@ fn main() {
} }
static APP: Component = |cx| { 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! { cx.render(rsx! {
div { Router {
{match route { onchange: move |route| log::info!("route changed to {}", route),
Route::Home => rsx!(h1 { "Home" }), Route { to: "/", Home {} }
Route::About => rsx!(h1 { "About" }), Route { to: "blog"
Route::NotFound => rsx!(h1 { "NotFound" }), Route { to: "/", BlogList {} }
}} Route { to: ":id", BlogPost {} }
nav {
Link { to: Route::Home, href: "/" }
Link { to: Route::About, href: "/about" }
} }
} }
}) })
}; };
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::<usize>("id")?;
cx.render(rsx! {
div {
}
})
}
@@ -1,4 +1,4 @@
use crate::{Routable, RouterService}; use crate::RouterService;
use dioxus::Attribute; use dioxus::Attribute;
use dioxus_core as dioxus; use dioxus_core as dioxus;
use dioxus_core::prelude::*; use dioxus_core::prelude::*;
@@ -6,8 +6,8 @@ use dioxus_core_macro::{format_args_f, rsx, Props};
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
#[derive(Props)] #[derive(Props)]
pub struct LinkProps<'a, R: Routable> { pub struct LinkProps<'a> {
to: R, to: &'a str,
/// The url that gets pushed to the history stack /// The url that gets pushed to the history stack
/// ///
@@ -28,6 +28,9 @@ pub struct LinkProps<'a, R: Routable> {
#[props(default, setter(strip_option))] #[props(default, setter(strip_option))]
class: Option<&'a str>, class: Option<&'a str>,
#[props(default, setter(strip_option))]
id: Option<&'a str>,
children: Element<'a>, children: Element<'a>,
#[props(default)] #[props(default)]
@@ -35,13 +38,21 @@ pub struct LinkProps<'a, R: Routable> {
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn Link<'a, R: Routable>(cx: Scope<'a, LinkProps<'a, R>>) -> Element { pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
let service = cx.consume_context::<RouterService<R>>()?; log::debug!("rendering link {:?}", cx.scope_id());
let service = cx.consume_context::<RouterService>()?;
cx.render(rsx! { cx.render(rsx! {
a { a {
href: "#", href: "{cx.props.to}",
class: format_args!("{}", cx.props.class.unwrap_or("")), 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 &cx.props.children
} }
}) })
+58
View File
@@ -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::<RouterService>())
.as_ref()?;
cx.use_hook(|_| {
// create a bigger, better, longer route if one above us exists
let total_route = match cx.consume_context::<RouteContext>() {
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!()
}
}
+29
View File
@@ -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 }
))
}
+30
View File
@@ -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<T>(&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<T>(&self, name: &str) -> Option<&T> {
todo!()
}
}
pub fn use_route(cx: &ScopeState) -> UseRoute {
todo!()
}
+20 -25
View File
@@ -7,17 +7,6 @@
//! //!
//! ```rust //! ```rust
//! fn app(cx: Scope) -> Element { //! 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 platform;
mod routecontext;
mod service; mod service;
mod userouter;
mod utils; mod utils;
pub use link::*; pub use routecontext::*;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
pub use service::*; pub use service::*;
pub use userouter::*;
pub trait Routable:
'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default
{
}
impl<T> Routable for T where
T: 'static + Send + Clone + PartialEq + Serialize + DeserializeOwned + Default
{
}
-7
View File
@@ -1,7 +0,0 @@
pub fn parse_current_route() -> String {
todo!()
}
pub(crate) fn get_base_route() -> String {
todo!()
}
+11
View File
@@ -0,0 +1,11 @@
use url::Url;
pub trait RouterProvider {
fn get_current_route(&self) -> String;
fn subscribe_to_route_changes(&self, callback: Box<dyn Fn(String)>);
}
enum RouteChange {
LinkTo(String),
Back(String),
}
+7
View File
@@ -0,0 +1,7 @@
pub struct RouteContext {
// "/name/:id"
pub declared_route: String,
// "app/name/:id"
pub total_route: String,
}
+68 -11
View File
@@ -1,18 +1,75 @@
use crate::Routable; use gloo::history::{BrowserHistory, History, HistoryListener};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, collections::HashMap, rc::Rc};
pub struct RouterService<R: Routable> { use dioxus_core::ScopeId;
pub(crate) regen_route: Rc<dyn Fn()>,
pub(crate) pending_routes: RefCell<Vec<R>>, pub struct RouterService {
pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
history: RefCell<BrowserHistory>,
registerd_routes: RefCell<RouteSlot>,
slots: RefCell<HashMap<ScopeId, String>>,
cur_root: RefCell<String>,
listener: HistoryListener,
} }
impl<R: Routable> RouterService<R> { enum RouteSlot {
pub fn current_path(&self) -> &str { Routes {
todo!() // the partial route
partial: String,
// the total route
total: String,
// Connections to other routs
rest: Vec<RouteSlot>,
},
}
impl RouterService {
pub fn new(regen_route: Rc<dyn Fn(ScopeId)>, root_scope: ScopeId) -> Self {
let history = BrowserHistory::default();
let location = history.location();
let path = location.path();
let regen = Rc::clone(&regen_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); pub fn push_route(&self, route: &str) {
(self.regen_route)(); 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,
}
} }
} }
-33
View File
@@ -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<R> = 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::<RouterService<R>>().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<R: Routable> {
svc: Rc<RouterService<R>>,
history: Vec<R>,
}
+15 -1
View File
@@ -274,7 +274,21 @@ impl WebsysDom {
// "Result" cannot be received from JS // "Result" cannot be received from JS
// Instead, we just build and immediately execute a closure that returns result // Instead, we just build and immediately execute a closure that returns result
match decode_trigger(event) { 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::<HtmlElement>() {
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), Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
}; };
}); });
+4
View File
@@ -157,6 +157,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback); let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback);
log::debug!("rebuilding app");
let mut mutations = dom.rebuild(); let mut mutations = dom.rebuild();
// hydrating is simply running the dom for a single render. If the page is already written, then the corresponding // 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<T: 'static + Send>(root: Component<T>, root_props: T
let work_loop = ric_raf::RafLoop::new(); let work_loop = ric_raf::RafLoop::new();
loop { loop {
log::debug!("waiting for work");
// if virtualdom has nothing, wait for it to have something before requesting idle time // if virtualdom has nothing, wait for it to have something before requesting idle time
// if there is work then this future resolves immediately. // if there is work then this future resolves immediately.
dom.wait_for_work().await; dom.wait_for_work().await;
log::debug!("working..");
// wait for the mainthread to schedule us in // wait for the mainthread to schedule us in
let mut deadline = work_loop.wait_for_idle_time().await; let mut deadline = work_loop.wait_for_idle_time().await;