From cfa0927cdd40bc3dba22996018605dbad91d0391 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 1 Apr 2021 00:01:42 -0400 Subject: [PATCH] feat: todomvc --- notes/ARCHITECTURE.md | 15 + notes/CHANGELOG.md | 31 +- packages/core-macro/src/rsxt.rs | 20 +- packages/core/examples/borrowed.rs | 1 + packages/core/examples/nested.rs | 1 + packages/core/src/diff.rs | 42 +- packages/core/src/events.rs | 4 +- packages/core/src/nodebuilder.rs | 76 +--- packages/core/src/nodes.rs | 34 +- packages/ssr/src/tostring.rs | 4 + packages/web/Cargo.toml | 4 + packages/web/examples/todomvc.rs | 280 +++++++------ .../web/examples/todomvc/filtertoggles.rs | 44 ++ packages/web/examples/todomvc/main.rs | 40 ++ packages/web/examples/todomvc/recoil.rs | 87 ++++ packages/web/examples/todomvc/state.rs | 27 ++ packages/web/examples/todomvc/style.css | 376 ++++++++++++++++++ packages/web/examples/todomvc/todoitem.rs | 36 ++ packages/web/examples/todomvc/todolist.rs | 49 +++ packages/web/src/lib.rs | 8 +- 20 files changed, 934 insertions(+), 245 deletions(-) create mode 100644 packages/web/examples/todomvc/filtertoggles.rs create mode 100644 packages/web/examples/todomvc/main.rs create mode 100644 packages/web/examples/todomvc/recoil.rs create mode 100644 packages/web/examples/todomvc/state.rs create mode 100644 packages/web/examples/todomvc/style.css create mode 100644 packages/web/examples/todomvc/todoitem.rs create mode 100644 packages/web/examples/todomvc/todolist.rs diff --git a/notes/ARCHITECTURE.md b/notes/ARCHITECTURE.md index 2f1c266c1..cd0140e54 100644 --- a/notes/ARCHITECTURE.md +++ b/notes/ARCHITECTURE.md @@ -1,3 +1,18 @@ # Dioxus Architecture :) + + +```rust + +let data = use_context(); +data.set(abc); + +unsafe { + // data is unsafely aliased + data.modify(|&mut data| { + + }) +} + +``` diff --git a/notes/CHANGELOG.md b/notes/CHANGELOG.md index 3ab2f409a..bfd74c64d 100644 --- a/notes/CHANGELOG.md +++ b/notes/CHANGELOG.md @@ -76,17 +76,20 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su ## Outstanding todos: > anything missed so far -- dirty tagging, compression -- fragments -- make ssr follow HTML spec -- code health -- miri tests -- todo mvc -- fix -- node refs (postpone for future release?) -- styling built-in (future release?) -- key handler? -- FC macro -- Documentation overhaul -- Website -- keys on components +- [ ] dirty tagging, compression +- [ ] fragments +- [ ] make ssr follow HTML spec +- [ ] code health +- [ ] miri tests +- [ ] todo mvc +- [ ] fix +- [ ] node refs (postpone for future release?) +- [ ] styling built-in (future release?) +- [ ] key handler? +- [ ] FC macro +- [ ] Documentation overhaul +- [ ] Website +- [x] keys on components +- [ ] fix keys on elements +- [ ] all synthetic events filed out +- [ ] doublecheck event targets and stuff diff --git a/packages/core-macro/src/rsxt.rs b/packages/core-macro/src/rsxt.rs index 19e123416..bb55fbb1e 100644 --- a/packages/core-macro/src/rsxt.rs +++ b/packages/core-macro/src/rsxt.rs @@ -157,16 +157,32 @@ impl ToTokens for &Component { fc_to_builder(#name) }; + let mut has_key = None; + for field in &self.body { - builder.append_all(quote! {#field}); + if field.name.to_string() == "key" { + has_key = Some(field); + } else { + builder.append_all(quote! {#field}); + } } builder.append_all(quote! { .build() }); + let key_token = match has_key { + Some(field) => { + let inners = field.content.to_token_stream(); + quote! { + Some(#inners) + } + } + None => quote! {None}, + }; + let _toks = tokens.append_all(quote! { - dioxus::builder::virtual_child(ctx, #name, #builder) + dioxus::builder::virtual_child(ctx, #name, #builder, #key_token) }); } } diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index 97b8ea801..56a1c3a6a 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -38,6 +38,7 @@ fn app<'a>(ctx: Context<'a>, props: &Props) -> DomTree { .item(child) .item_handler(set_val) .build(), + None, )); } root.finish() diff --git a/packages/core/examples/nested.rs b/packages/core/examples/nested.rs index dc7acb5a7..dc11cf361 100644 --- a/packages/core/examples/nested.rs +++ b/packages/core/examples/nested.rs @@ -18,6 +18,7 @@ static Header: FC<()> = |ctx, props| { Bottom, // c.bump.alloc(()), + None, ))) .finish() })) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 643730183..ed977bc65 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -473,27 +473,27 @@ impl<'a> DiffMachine<'a> { // // Upon exiting, the change list stack is in the same state. fn diff_keyed_children(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) { - if cfg!(debug_assertions) { - let mut keys = fxhash::FxHashSet::default(); - let mut assert_unique_keys = |children: &[VNode]| { - keys.clear(); - for child in children { - let key = child.key(); - debug_assert!( - key.is_some(), - "if any sibling is keyed, all siblings must be keyed" - ); - keys.insert(key); - } - debug_assert_eq!( - children.len(), - keys.len(), - "keyed siblings must each have a unique key" - ); - }; - assert_unique_keys(old); - assert_unique_keys(new); - } + // if cfg!(debug_assertions) { + // let mut keys = fxhash::FxHashSet::default(); + // let mut assert_unique_keys = |children: &[VNode]| { + // keys.clear(); + // for child in children { + // let key = child.key(); + // debug_assert!( + // key.is_some(), + // "if any sibling is keyed, all siblings must be keyed" + // ); + // keys.insert(key); + // } + // debug_assert_eq!( + // children.len(), + // keys.len(), + // "keyed siblings must each have a unique key" + // ); + // }; + // assert_unique_keys(old); + // assert_unique_keys(new); + // } // First up, we diff all the nodes with the same key at the beginning of the // children. diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 1aa0a2ac3..9c04e95fe 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -146,7 +146,9 @@ pub mod on { } #[derive(Debug)] - pub struct FormEvent {} + pub struct FormEvent { + pub value: String, + } event_builder! { FormEvent; change input invalid reset submit diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index 3e0a04173..8ed6b5f42 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -23,7 +23,7 @@ where Children: 'a + AsRef<[VNode<'a>]>, { ctx: &'b NodeCtx<'a>, - key: NodeKey, + key: NodeKey<'a>, tag_name: &'a str, listeners: Listeners, attributes: Attributes, @@ -264,10 +264,8 @@ where /// .finish(); /// ``` #[inline] - pub fn key(mut self, key: u32) -> Self { - use std::u32; - debug_assert!(key != u32::MAX); - self.key = NodeKey(key); + pub fn key(mut self, key: &'a str) -> Self { + self.key = NodeKey(Some(key)); self } @@ -572,65 +570,6 @@ impl<'a> IntoDomTree<'a> for () { } } -#[test] -fn test_iterator_of_nodes<'b>() { - use crate::prelude::*; - - // static Example: FC<()> = |ctx, props| { - // let body = rsx! { - // div {} - // }; - - // ctx.render(rsx! { - // div { - // h1 {} - // } - // }) - // }; - - // let p = (0..10).map(|f| { - // // - // LazyNodes::new(rsx! { - // div { - // "aaa {f}" - // } - // }) - // }); - - // let g = p.into_iter(); - // for f in g {} - - // static Example: FC<()> = |ctx, props| { - // ctx.render(|c| { - // // - // ElementBuilder::new(c, "div") - // .iter_child({ - // // rsx! - // LazyNodes::new(move |n: &NodeCtx| -> VNode { - // // - // ElementBuilder::new(n, "div").finish() - // }) - // }) - // .iter_child({ - // // render to wrapper -> tree - // ctx.render(rsx! { - // div {} - // }) - // }) - // .iter_child({ - // // map rsx! - // (0..10).map(|f| { - // LazyNodes::new(move |n: &NodeCtx| -> VNode { - // // - // ElementBuilder::new(n, "div").finish() - // }) - // }) - // }) - // .finish() - // }) - // }; -} - /// Construct a text VNode. /// /// This is `dioxus`'s virtual DOM equivalent of `document.createTextVNode`. @@ -668,9 +607,14 @@ pub fn attr<'a>(name: &'static str, value: &'a str) -> Attribute<'a> { Attribute { name, value } } -pub fn virtual_child<'a, T: Properties + 'a>(ctx: &NodeCtx<'a>, f: FC, p: T) -> VNode<'a> { +pub fn virtual_child<'a, T: Properties + 'a>( + ctx: &NodeCtx<'a>, + f: FC, + p: T, + key: Option<&'a str>, // key: NodeKey<'a>, +) -> VNode<'a> { // currently concerned about if props have a custom drop implementation // might override it with the props macro let propsd: &'a mut _ = ctx.bump.alloc(p); - VNode::Component(crate::nodes::VComponent::new(f, propsd)) + VNode::Component(crate::nodes::VComponent::new(f, propsd, key)) } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 0a9b8670f..4e5816ed2 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -59,7 +59,7 @@ impl<'a> VNode<'a> { #[inline] pub fn element( bump: &'a Bump, - key: NodeKey, + key: NodeKey<'a>, tag_name: &'a str, listeners: &'a [Listener<'a>], attributes: &'a [Attribute<'a>], @@ -102,7 +102,7 @@ impl<'a> VNode<'a> { #[derive(Debug)] pub struct VElement<'a> { /// Elements have a tag name, zero or more attributes, and zero or more - pub key: NodeKey, + pub key: NodeKey<'a>, pub tag_name: &'a str, pub listeners: &'a [Listener<'a>], pub attributes: &'a [Attribute<'a>], @@ -191,16 +191,16 @@ impl Debug for Listener<'_> { /// /// If any sibling is keyed, then they all must be keyed. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct NodeKey(pub(crate) u32); +pub struct NodeKey<'a>(pub(crate) Option<&'a str>); -impl Default for NodeKey { - fn default() -> NodeKey { +impl<'a> Default for NodeKey<'a> { + fn default() -> NodeKey<'a> { NodeKey::NONE } } -impl NodeKey { +impl<'a> NodeKey<'a> { /// The default, lack of a key. - pub const NONE: NodeKey = NodeKey(u32::MAX); + pub const NONE: NodeKey<'a> = NodeKey(None); /// Is this key `NodeKey::NONE`? #[inline] @@ -218,9 +218,8 @@ impl NodeKey { /// /// `key` must not be `u32::MAX`. #[inline] - pub fn new(key: u32) -> Self { - debug_assert_ne!(key, u32::MAX); - NodeKey(key) + pub fn new(key: &'a str) -> Self { + NodeKey(Some(key)) } } @@ -231,9 +230,7 @@ pub struct VText<'bump> { impl<'a> VText<'a> { // / Create an new `VText` instance with the specified text. - pub fn new(text: &'a str) -> Self -// pub fn new(text: Into) -> Self - { + pub fn new(text: &'a str) -> Self { VText { text: text.into() } } } @@ -248,7 +245,7 @@ pub type StableScopeAddres = RefCell>; pub type VCompAssociatedScope = RefCell>; pub struct VComponent<'src> { - pub key: NodeKey, + pub key: NodeKey<'src>, pub stable_addr: Rc, pub ass_scope: Rc, @@ -276,7 +273,7 @@ impl<'a> VComponent<'a> { // - perform comparisons when diffing (memoization) // TODO: lift the requirement that props need to be static // we want them to borrow references... maybe force implementing a "to_static_unsafe" trait - pub fn new(component: FC

, props: &'a P) -> Self { + pub fn new(component: FC

, props: &'a P, key: Option<&'a str>) -> Self { let caller_ref = component as *const (); let raw_props = props as *const P as *const (); @@ -299,8 +296,13 @@ impl<'a> VComponent<'a> { let caller = Rc::new(create_closure(component, raw_props)); + let key = match key { + Some(key) => NodeKey::new(key), + None => NodeKey(None), + }; + Self { - key: NodeKey::NONE, + key, ass_scope: Rc::new(RefCell::new(None)), user_fc: caller_ref, raw_props: props as *const P as *const _, diff --git a/packages/ssr/src/tostring.rs b/packages/ssr/src/tostring.rs index cd6f21724..0e076983c 100644 --- a/packages/ssr/src/tostring.rs +++ b/packages/ssr/src/tostring.rs @@ -53,6 +53,9 @@ fn html_render( #[test] fn test_serialize() { let mut dom = VirtualDom::new(|ctx, props| { + // + // + // ctx.render(rsx! { div { title: "About W3Schools" @@ -70,6 +73,7 @@ fn test_serialize() { } }) }); + dom.rebuild(); let renderer = SsrRenderer { dom }; diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 9862f8f7f..066dce4cd 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -59,3 +59,7 @@ crate-type = ["cdylib", "rlib"] [dev-dependencies] uuid = { version = "0.8.2", features = ["v4"] } + +[[example]] +name = "todomvc" +path = "./examples/todomvc/main.rs" diff --git a/packages/web/examples/todomvc.rs b/packages/web/examples/todomvc.rs index 9a8a4b761..916887ec9 100644 --- a/packages/web/examples/todomvc.rs +++ b/packages/web/examples/todomvc.rs @@ -1,75 +1,49 @@ -use std::{ - collections::{BTreeMap, BTreeSet}, - sync::atomic::AtomicUsize, -}; - use dioxus_core::prelude::*; use dioxus_web::WebsysRenderer; +use recoil::{use_recoil_callback, RecoilContext}; use uuid::Uuid; -// Entry point +static TODOS: AtomFamily = atom_family(|_| {}); + fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|ctx, props| { - ctx.create_context(|| model::TodoManager::new()); + let global_reducer = use_recoil_callback(|| ()); + + let todos = use_atom(TODOS).iter().map(|(order, item)| { + rsx!(TodoItem { + key: "{order}", + id: item.id, + }) + }); ctx.render(rsx! { div { - TodoList {} + {todos} Footer {} } }) })) } -static TodoList: FC<()> = |ctx, props| { - let todos = use_state_new(&ctx, || BTreeMap::::new()); - - let items = todos.iter().map(|(order, item)| { - rsx!(TodoItem { - // key: "{}", - todo: item - }) - }); - - ctx.render(rsx! { - div { - {items} - } - }) -}; - -#[derive(Debug, PartialEq, Props)] -struct TodoItemsProp<'a> { - todo: &'a model::TodoItem, +#[derive(Debug, PartialEq, Clone)] +pub struct TodoItem { + pub id: Uuid, + pub checked: bool, + pub contents: String, } -fn TodoItem(ctx: Context, props: &TodoItemsProp) -> DomTree { - let (editing, set_editing) = use_state(&ctx, || false); - - let id = props.todo.id; - ctx.render(rsx! ( - li { - div { - "{id}" - } - // {input} - } - )) +// build a global context for the app +// as we scale the app, we can create separate, stateful impls +impl RecoilContext<()> { + fn add_todo(&self) {} + fn remove_todo(&self) {} + fn select_all_todos(&self) {} } -static Footer: FC<()> = |ctx, props| { - ctx.render(html! { -

- }) -}; +mod hooks { + use super::*; + fn use_keyboard_shortcuts(ctx: &Context) {} +} // The data model that the todo mvc uses mod model { @@ -84,12 +58,26 @@ mod model { pub contents: String, } - struct Dispatcher {} + fn atom() {} + + // struct Dispatcher {} struct AppContext { _t: std::rc::Rc, } + // pub fn use_appcontext(ctx: &Context, f: impl FnOnce() -> T) -> AppContext { + // todo!() + // } + + // static TodoList: ContextFamily = context_family(); + + // struct TodoBoss<'a> {} + + // fn use_recoil_todos() -> TodoBoss {} + + // pub fn use_context_family(ctx: &Context) {} + impl AppContext { fn dispatch(&self, f: impl FnOnce(&mut T)) {} fn async_dispatch(&self, f: impl Future) {} @@ -101,78 +89,130 @@ mod model { } } - // use im-rc if your contexts are too large to clone! - // or, dangerously mutate and update subscriptions manually - #[derive(Clone)] - pub struct TodoManager { - items: Vec, + // // use im-rc if your contexts are too large to clone! + // // or, dangerously mutate and update subscriptions manually + // #[derive(Clone, Debug, PartialEq)] + // pub struct TodoManager { + // items: Vec, + // } + + // // App context is an ergonomic way of sharing data models through a tall tree + // // Because it holds onto the source data with Rc, it's cheap to clone through props and allows advanced memoization + // // It's particularly useful when moving through tall trees, or iterating through complex data models. + // // By wrapping the source type, we can forward any mutation through "dispatch", making it clear when clones occur. + // // This also enables traditional method-style + // impl AppContext { + // fn get_todos(&self, ctx: &Context) {} + + // fn remove_todo(&self, id: Uuid) { + // self.dispatch(|f| { + // // todos... remove + // }) + // } + + // async fn push_todo(&self, todo: TodoItem) { + // self.dispatch(|f| { + // // + // f.items.push(10); + // }); + // } + + // fn add_todo(&self) { + // // self.dispatch(|f| {}); + // // let items = self.get(|f| &f.items); + // } + // } + + // pub enum TodoActions {} + // impl TodoManager { + // pub fn reduce(s: &mut Rc, action: TodoActions) { + // match action { + // _ => {} + // } + // } + + // pub fn new() -> Rc { + // todo!() + // } + + // pub fn get_todo(&self, id: Uuid) -> &TodoItem { + // todo!() + // } + + // pub fn get_todos(&self) -> &BTreeMap { + // todo!() + // } + // } + + // pub struct TodoHandle {} + // impl TodoHandle { + // fn get_todo(&self, id: Uuid) -> &TodoItem { + // todo!() + // } + + // fn add_todo(&self, todo: TodoItem) {} + // } + + // // use_reducer, but exposes the reducer and context to children + // fn use_reducer_context() {} + // fn use_context_selector() {} + + // fn use_context<'b, 'c, Root: 'static, Item: 'c>( + // ctx: &'b Context<'c>, + // f: impl Fn(Root) -> &'c Item, + // ) -> &'c Item { + // todo!() + // } + + // pub fn use_todo_item<'b, 'c>(ctx: &'b Context<'c>, item: Uuid) -> &'c TodoItem { + // todo!() + // // ctx.use_hook(|| TodoManager::new(), |hook| {}, cleanup) + // } + // fn use_todos(ctx: &Context) -> TodoHandle { + // todo!() + // } + + // fn use_todo_context(ctx: &Context) -> AppContext { + // todo!() + // } + + // fn test(ctx: Context) { + // let todos = use_todos(&ctx); + // let todo = todos.get_todo(Uuid::new_v4()); + + // let c = use_todo_context(&ctx); + // // todos.add_todo(); + // } +} + +mod recoil { + + pub struct RecoilContext { + _inner: T, } - impl AppContext { - fn remove_todo(&self, id: Uuid) { - self.dispatch(|f| {}) - } + impl RecoilContext { + /// Get the value of an atom. Returns a reference to the underlying data. - async fn push_todo(&self, todo: TodoItem) { - self.dispatch(|f| { - // - f.items.push(10); - }); - } + pub fn get(&self) {} - fn add_todo(&self) { - // self.dispatch(|f| {}); - // let items = self.get(|f| &f.items); - } + /// Replace an existing value with a new value + /// + /// This does not replace the value instantly, and all calls to "get" within the current scope will return + pub fn set(&self) {} + + // Modify lets you modify the value in place. However, because there's no previous value around to compare + // the new one with, we are unable to memoize the change. As such, all downsteam users of this Atom will + // be updated, causing all subsrcibed components to re-render. + // + // This is fine for most values, but might not be performant when dealing with collections. For collections, + // use the "Family" variants as these will stay memoized for inserts, removals, and modifications. + // + // Note - like "set" this won't propogate instantly. Once all "gets" are dropped, only then will we run the + pub fn modify(&self) {} } - impl TodoManager { - pub fn new() -> Self { - todo!() - } - - pub fn get_todo(&self) -> &TodoItem { - todo!() - } - } - - pub struct TodoHandle {} - impl TodoHandle { - fn get_todo(&self, id: Uuid) -> &TodoItem { - todo!() - } - - fn add_todo(&self, todo: TodoItem) {} - } - - // use_reducer, but exposes the reducer and context to children - fn use_reducer_context() {} - fn use_context_selector() {} - - fn use_context<'b, 'c, Root: 'static, Item: 'c>( - ctx: &'b Context<'c>, - f: impl Fn(Root) -> &'c Item, - ) -> &'c Item { + pub fn use_recoil_callback(f: impl Fn() -> G) -> RecoilContext { todo!() } - - pub fn use_todo_item<'b, 'c>(ctx: &'b Context<'c>, item: Uuid) -> &'c TodoItem { - todo!() - // ctx.use_hook(|| TodoManager::new(), |hook| {}, cleanup) - } - fn use_todos(ctx: &Context) -> TodoHandle { - todo!() - } - - fn use_todo_context(ctx: &Context) -> AppContext { - todo!() - } - - fn test(ctx: Context) { - let todos = use_todos(&ctx); - let todo = todos.get_todo(Uuid::new_v4()); - - let c = use_todo_context(&ctx); - // todos.add_todo(); - } } diff --git a/packages/web/examples/todomvc/filtertoggles.rs b/packages/web/examples/todomvc/filtertoggles.rs new file mode 100644 index 000000000..c806b9388 --- /dev/null +++ b/packages/web/examples/todomvc/filtertoggles.rs @@ -0,0 +1,44 @@ +use crate::recoil; +use crate::state::{FilterState, TODOS}; +use dioxus_core::prelude::*; + +pub fn FilterToggles(ctx: Context, props: &()) -> DomTree { + let reducer = recoil::use_callback(&ctx, || ()); + let items_left = recoil::use_atom_family(&ctx, &TODOS, uuid::Uuid::new_v4()); + + let toggles = [ + ("All", "", FilterState::All), + ("Active", "active", FilterState::Active), + ("Completed", "completed", FilterState::Completed), + ] + .iter() + .map(|(name, path, filter)| { + rsx! { + li { + class: "{name}" + a { + href: "{path}" + onclick: move |_| reducer.set_filter(&filter) + "{name}" + } + } + } + }); + + // todo + let item_text = ""; + let items_left = ""; + + ctx.render(rsx! { + footer { + span { + strong {"{items_left}"} + span {"{item_text} left"} + } + ul { + class: "filters" + {toggles} + } + } + }) +} diff --git a/packages/web/examples/todomvc/main.rs b/packages/web/examples/todomvc/main.rs new file mode 100644 index 000000000..37ac660bf --- /dev/null +++ b/packages/web/examples/todomvc/main.rs @@ -0,0 +1,40 @@ +use dioxus_core::prelude::*; +use dioxus_web::WebsysRenderer; + +mod filtertoggles; +mod recoil; +mod state; +mod todoitem; +mod todolist; + +use todolist::TodoList; + +static APP_STYLE: &'static str = include_str!("./style.css"); + +fn main() { + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|ctx, _| { + ctx.render(rsx! { + div { + id: "app" + style { "{APP_STYLE}" } + + // list + TodoList {} + + // footer + footer { + class: "info" + p {"Double-click to edit a todo"} + p { + "Created by " + a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" } + } + p { + "Part of " + a { "TodoMVC", href: "http://todomvc.com" } + } + } + } + }) + })) +} diff --git a/packages/web/examples/todomvc/recoil.rs b/packages/web/examples/todomvc/recoil.rs new file mode 100644 index 000000000..a92f79b57 --- /dev/null +++ b/packages/web/examples/todomvc/recoil.rs @@ -0,0 +1,87 @@ +use dioxus_core::context::Context; + +pub struct RecoilContext { + _inner: T, +} + +impl RecoilContext { + /// Get the value of an atom. Returns a reference to the underlying data. + + pub fn get(&self) {} + + /// Replace an existing value with a new value + /// + /// This does not replace the value instantly, and all calls to "get" within the current scope will return + pub fn set(&self) {} + + // Modify lets you modify the value in place. However, because there's no previous value around to compare + // the new one with, we are unable to memoize the change. As such, all downsteam users of this Atom will + // be updated, causing all subsrcibed components to re-render. + // + // This is fine for most values, but might not be performant when dealing with collections. For collections, + // use the "Family" variants as these will stay memoized for inserts, removals, and modifications. + // + // Note - like "set" this won't propogate instantly. Once all "gets" are dropped, only then will we run the + pub fn modify(&self) {} +} + +pub fn use_callback<'a, G>(c: &Context<'a>, f: impl Fn() -> G) -> &'a RecoilContext { + todo!() +} + +pub fn use_atom(c: &Context, t: &'static Atom) -> O { + todo!() +} +pub fn use_batom(c: &Context, t: impl Readable) -> O { + todo!() +} + +pub trait Readable {} +impl Readable for &'static Atom {} +impl Readable for &'static AtomFamily {} + +pub fn use_atom_family<'a, K: PartialEq, V: PartialEq>( + c: &Context<'a>, + t: &'static AtomFamily, + g: K, +) -> &'a V { + todo!() +} + +pub use atoms::{atom, Atom}; +pub use atoms::{atom_family, AtomFamily}; +mod atoms { + + use super::*; + pub struct AtomBuilder { + pub key: String, + pub manual_init: Option T>>, + _never: std::marker::PhantomData, + } + + impl AtomBuilder { + pub fn new() -> Self { + Self { + key: uuid::Uuid::new_v4().to_string(), + manual_init: None, + _never: std::marker::PhantomData {}, + } + } + + pub fn init T + 'static>(&mut self, f: A) { + self.manual_init = Some(Box::new(f)); + } + + pub fn set_key(&mut self, _key: &'static str) {} + } + + pub struct atom(pub fn(&mut AtomBuilder) -> T); + pub type Atom = atom; + + pub struct AtomFamilyBuilder { + _never: std::marker::PhantomData<(K, V)>, + } + + pub struct atom_family(pub fn(&mut AtomFamilyBuilder)); + pub type AtomFamily = atom_family; +} diff --git a/packages/web/examples/todomvc/state.rs b/packages/web/examples/todomvc/state.rs new file mode 100644 index 000000000..3bc794d68 --- /dev/null +++ b/packages/web/examples/todomvc/state.rs @@ -0,0 +1,27 @@ +use crate::recoil::*; + +pub static TODOS: AtomFamily = atom_family(|_| {}); +pub static FILTER: Atom = atom(|_| FilterState::All); + +#[derive(PartialEq)] +pub enum FilterState { + All, + Active, + Completed, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct TodoItem { + pub id: uuid::Uuid, + pub checked: bool, + pub contents: String, +} + +impl crate::recoil::RecoilContext<()> { + pub fn add_todo(&self, contents: String) {} + pub fn remove_todo(&self) {} + pub fn select_all_todos(&self) {} + pub fn toggle_todo(&self, id: uuid::Uuid) {} + pub fn clear_completed(&self) {} + pub fn set_filter(&self, filter: &FilterState) {} +} diff --git a/packages/web/examples/todomvc/style.css b/packages/web/examples/todomvc/style.css new file mode 100644 index 000000000..d8be205ad --- /dev/null +++ b/packages/web/examples/todomvc/style.css @@ -0,0 +1,376 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + text-align: center; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; +} + +.toggle-all + label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all + label:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked + label:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/packages/web/examples/todomvc/todoitem.rs b/packages/web/examples/todomvc/todoitem.rs new file mode 100644 index 000000000..f3cb9259a --- /dev/null +++ b/packages/web/examples/todomvc/todoitem.rs @@ -0,0 +1,36 @@ +use super::state::TODOS; +use crate::recoil::use_atom_family; +use dioxus_core::prelude::*; +use uuid::Uuid; + +#[derive(PartialEq, Props)] +pub struct TodoEntryProps { + id: Uuid, +} + +pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree { + let (is_editing, set_is_editing) = use_state(&ctx, || false); + let todo = use_atom_family(&ctx, &TODOS, props.id); + + ctx.render(rsx! ( + li { + "{todo.id}" + input { + class: "toggle" + type: "checkbox" + "{todo.checked}" + } + {is_editing.then(|| { + rsx!(input { + value: "{contents}" + }) + })} + } + )) +} + +pub fn Example(ctx: Context, id: Uuid, name: String) -> DomTree { + ctx.render(rsx! { + div {} + }) +} diff --git a/packages/web/examples/todomvc/todolist.rs b/packages/web/examples/todomvc/todolist.rs new file mode 100644 index 000000000..1164ca363 --- /dev/null +++ b/packages/web/examples/todomvc/todolist.rs @@ -0,0 +1,49 @@ +use super::state::{FilterState, TodoItem, FILTER, TODOS}; +use crate::filtertoggles::FilterToggles; +use crate::recoil::use_atom; +use crate::todoitem::TodoEntry; +use dioxus_core::prelude::*; + +pub fn TodoList(ctx: Context, props: &()) -> DomTree { + let (entry, set_entry) = use_state(&ctx, || "".to_string()); + let todos: &Vec = todo!(); + let filter = use_atom(&ctx, &FILTER); + + let list = todos + .iter() + .filter(|f| match filter { + FilterState::All => true, + FilterState::Active => !f.checked, + FilterState::Completed => f.checked, + }) + .map(|item| { + rsx!(TodoEntry { + key: "{order}", + id: item.id, + }) + }); + + ctx.render(rsx! { + div { + // header + header { + class: "header" + h1 {"todos"} + input { + class: "new-todo" + placeholder: "What needs to be done?" + value: "{entry}" + oninput: move |evt| set_entry(evt.value) + } + } + + // list + {list} + + // filter toggle (show only if the list isn't empty) + {(!todos.is_empty()).then(|| + rsx!{ FilterToggles {} + })} + } + }) +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 3d5ff8fd3..22d0106b1 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -33,7 +33,7 @@ impl WebsysRenderer { /// Run the app to completion, panicing if any error occurs while rendering. /// Pairs well with the wasm_bindgen async handler pub async fn start(root: FC<()>) { - Self::new(root).run().await.expect("Virtual DOM failed"); + Self::new(root).run().await.expect("Virtual DOM failed :("); } /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component. @@ -43,6 +43,7 @@ impl WebsysRenderer { pub fn new(root: FC<()>) -> Self { Self::new_with_props(root, ()) } + /// Create a new text-renderer instance from a functional component root. /// Automatically progresses the creation of the VNode tree to completion. /// @@ -53,7 +54,6 @@ impl WebsysRenderer { /// Create a new text renderer from an existing Virtual DOM. pub fn from_vdom(dom: VirtualDom) -> Self { - // todo: initialize the event registry properly Self { internal_dom: dom } } @@ -81,12 +81,10 @@ impl WebsysRenderer { patch_machine.handle_edit(edit); }); - - patch_machine.reset(); let root_node = body_element.first_child().unwrap(); patch_machine.stack.push(root_node.clone()); - + // log::debug!("patch stack size {:?}", patch_machine.stack); // Event loop waits for the receiver to finish up