فهرست منبع

feat: todomvc

Jonathan Kelley 4 سال پیش
والد
کامیت
cfa0927

+ 15 - 0
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| {
+        
+    })
+}
+
+```

+ 17 - 14
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

+ 18 - 2
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)
         });
     }
 }

+ 1 - 0
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()

+ 1 - 0
packages/core/examples/nested.rs

@@ -18,6 +18,7 @@ static Header: FC<()> = |ctx, props| {
                 Bottom,
                 //
                 c.bump.alloc(()),
+                None,
             )))
             .finish()
     }))

+ 21 - 21
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.

+ 3 - 1
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

+ 10 - 66
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<T>, p: T) -> VNode<'a> {
+pub fn virtual_child<'a, T: Properties + 'a>(
+    ctx: &NodeCtx<'a>,
+    f: FC<T>,
+    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))
 }

+ 18 - 16
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<str>) -> Self
-    {
+    pub fn new(text: &'a str) -> Self {
         VText { text: text.into() }
     }
 }
@@ -248,7 +245,7 @@ pub type StableScopeAddres = RefCell<Option<u32>>;
 pub type VCompAssociatedScope = RefCell<Option<ScopeIdx>>;
 
 pub struct VComponent<'src> {
-    pub key: NodeKey,
+    pub key: NodeKey<'src>,
 
     pub stable_addr: Rc<StableScopeAddres>,
     pub ass_scope: Rc<VCompAssociatedScope>,
@@ -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<P: Properties + 'a>(component: FC<P>, props: &'a P) -> Self {
+    pub fn new<P: Properties + 'a>(component: FC<P>, 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 _,

+ 4 - 0
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 };
 

+ 4 - 0
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"

+ 160 - 120
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<Uuid, TodoItem> = 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::<usize, model::TodoItem>::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! {
-        <footer className="info">
-            <p>"Double-click to edit a todo"</p>
-            <p>
-                "Created by "<a href="http://github.com/jkelleyrtp/">"jkelleyrtp"</a>
-            </p>
-            <p>
-                "Part of "<a href="http://todomvc.com">"TodoMVC"</a>
-            </p>
-        </footer>
-    })
-};
+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: Clone> {
         _t: std::rc::Rc<T>,
     }
 
+    // pub fn use_appcontext<T: Clone>(ctx: &Context, f: impl FnOnce() -> T) -> AppContext<T> {
+    //     todo!()
+    // }
+
+    // static TodoList: ContextFamily = context_family();
+
+    // struct TodoBoss<'a> {}
+
+    // fn use_recoil_todos() -> TodoBoss {}
+
+    // pub fn use_context_family(ctx: &Context) {}
+
     impl<T: Clone> AppContext<T> {
         fn dispatch(&self, f: impl FnOnce(&mut T)) {}
         fn async_dispatch(&self, f: impl Future<Output = ()>) {}
@@ -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<u32>,
-    }
-
-    impl AppContext<TodoManager> {
-        fn remove_todo(&self, id: Uuid) {
-            self.dispatch(|f| {})
-        }
-
-        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);
-        }
-    }
-
-    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 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<u32>,
+    // }
+
+    // // 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<TodoManager> {
+    //     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<Self>, action: TodoActions) {
+    //         match action {
+    //             _ => {}
+    //         }
+    //     }
+
+    //     pub fn new() -> Rc<Self> {
+    //         todo!()
+    //     }
+
+    //     pub fn get_todo(&self, id: Uuid) -> &TodoItem {
+    //         todo!()
+    //     }
+
+    //     pub fn get_todos(&self) -> &BTreeMap<String, 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 {
+    //     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<TodoManager> {
+    //     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();
+    // }
+}
 
-    // use_reducer, but exposes the reducer and context to children
-    fn use_reducer_context() {}
-    fn use_context_selector() {}
+mod recoil {
 
-    fn use_context<'b, 'c, Root: 'static, Item: 'c>(
-        ctx: &'b Context<'c>,
-        f: impl Fn(Root) -> &'c Item,
-    ) -> &'c Item {
-        todo!()
+    pub struct RecoilContext<T: 'static> {
+        _inner: T,
     }
 
-    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!()
+    impl<T: 'static> RecoilContext<T> {
+        /// 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) {}
     }
 
-    fn use_todo_context(ctx: &Context) -> AppContext<TodoManager> {
+    pub fn use_recoil_callback<G>(f: impl Fn() -> G) -> RecoilContext<G> {
         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();
-    }
 }

+ 44 - 0
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}
+            }
+        }
+    })
+}

+ 40 - 0
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" }
+                    }
+                }
+            }
+        })
+    }))
+}

+ 87 - 0
packages/web/examples/todomvc/recoil.rs

@@ -0,0 +1,87 @@
+use dioxus_core::context::Context;
+
+pub struct RecoilContext<T: 'static> {
+    _inner: T,
+}
+
+impl<T: 'static> RecoilContext<T> {
+    /// 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<G> {
+    todo!()
+}
+
+pub fn use_atom<T: PartialEq, O>(c: &Context, t: &'static Atom<T>) -> O {
+    todo!()
+}
+pub fn use_batom<T: PartialEq, O>(c: &Context, t: impl Readable) -> O {
+    todo!()
+}
+
+pub trait Readable {}
+impl<T: PartialEq> Readable for &'static Atom<T> {}
+impl<K: PartialEq, V: PartialEq> Readable for &'static AtomFamily<K, V> {}
+
+pub fn use_atom_family<'a, K: PartialEq, V: PartialEq>(
+    c: &Context<'a>,
+    t: &'static AtomFamily<K, V>,
+    g: K,
+) -> &'a V {
+    todo!()
+}
+
+pub use atoms::{atom, Atom};
+pub use atoms::{atom_family, AtomFamily};
+mod atoms {
+
+    use super::*;
+    pub struct AtomBuilder<T: PartialEq> {
+        pub key: String,
+        pub manual_init: Option<Box<dyn Fn() -> T>>,
+        _never: std::marker::PhantomData<T>,
+    }
+
+    impl<T: PartialEq> AtomBuilder<T> {
+        pub fn new() -> Self {
+            Self {
+                key: uuid::Uuid::new_v4().to_string(),
+                manual_init: None,
+                _never: std::marker::PhantomData {},
+            }
+        }
+
+        pub fn init<A: Fn() -> 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<T: PartialEq>(pub fn(&mut AtomBuilder<T>) -> T);
+    pub type Atom<T: PartialEq> = atom<T>;
+
+    pub struct AtomFamilyBuilder<K, V> {
+        _never: std::marker::PhantomData<(K, V)>,
+    }
+
+    pub struct atom_family<K: PartialEq, V: PartialEq>(pub fn(&mut AtomFamilyBuilder<K, V>));
+    pub type AtomFamily<K: PartialEq, V: PartialEq> = atom_family<K, V>;
+}

+ 27 - 0
packages/web/examples/todomvc/state.rs

@@ -0,0 +1,27 @@
+use crate::recoil::*;
+
+pub static TODOS: AtomFamily<uuid::Uuid, TodoItem> = atom_family(|_| {});
+pub static FILTER: Atom<FilterState> = 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) {}
+}

+ 376 - 0
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;
+	}
+}

+ 36 - 0
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 {}
+    })
+}

+ 49 - 0
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<TodoItem> = 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 {}
+            })}
+        }
+    })
+}

+ 3 - 5
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