From 61a64aa68be574308c39e40ef9453f1d2dbcd00f Mon Sep 17 00:00:00 2001 From: DavidOConnor Date: Mon, 8 Apr 2019 19:38:21 -0400 Subject: [PATCH] Fixed a bug in window events. Top-level children now mount direclty to the mount point instead of an intermediate element --- CHANGELOG.md | 6 ++ Cargo.toml | 3 +- README.md | 46 +++++----- examples/layered_structure/Cargo.toml | 13 --- examples/layered_structure/README.md | 3 - examples/layered_structure/build.ps1 | 5 -- examples/layered_structure/build.sh | 7 -- examples/layered_structure/index.html | 36 -------- examples/layered_structure/pkg/.keep | 0 examples/layered_structure/serve.py | 32 ------- examples/layered_structure/src/lib.rs | 99 -------------------- examples/todomvc/src/lib.rs | 6 +- proc_macros/Cargo.toml | 10 --- proc_macros/src/lib.rs | 11 --- src/lib.rs | 4 +- src/vdom.rs | 125 +++++++++++++------------- src/websys_bridge.rs | 17 ---- 17 files changed, 96 insertions(+), 327 deletions(-) delete mode 100644 examples/layered_structure/Cargo.toml delete mode 100644 examples/layered_structure/README.md delete mode 100644 examples/layered_structure/build.ps1 delete mode 100644 examples/layered_structure/build.sh delete mode 100644 examples/layered_structure/index.html delete mode 100644 examples/layered_structure/pkg/.keep delete mode 100644 examples/layered_structure/serve.py delete mode 100644 examples/layered_structure/src/lib.rs delete mode 100644 proc_macros/Cargo.toml delete mode 100644 proc_macros/src/lib.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f8d3d45..1db16c57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.3.1 +- `Top level view functions now return `Vec>` instead of `El`, mounted directly to + the mount point. (Breaking) +- `push_route()` can now accept a `Vec<&str>`, depreciating `push_path()`. +- Fixed a bug where window events couldn't be enabled on initialization + ## v0.3.0 - `update` function now takes a mutable ref of the model. (Breaking) - `Update` (update's return type) is now a struct. (Breaking) diff --git a/Cargo.toml b/Cargo.toml index 22bd8aa8d..b913dc939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seed" -version = "0.3.0" +version = "0.3.1" description = "A Rust framework for creating web apps, using WebAssembly" authors = ["DavidOConnor "] license = "MIT" @@ -79,7 +79,6 @@ members = [ "examples/todomvc", "examples/window_events", "examples/websocket", - "proc_macros", ] exclude = [ diff --git a/README.md b/README.md index a6a5eb6d2..f7c7b709f 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ fn success_level(clicks: i32) -> El { } /// The top-level component we pass to the virtual dom. -fn view(model: &Model) -> El { +fn view(model: &Model) -> Vec> { let plural = if model.count == 1 {""} else {"s"}; // Attrs, Style, Events, and children may be defined separately. @@ -155,27 +155,29 @@ fn view(model: &Model) -> El { "text-align" => "center" }; - div![ outer_style, - h1![ "The Grand Total" ], - div![ - style!{ - // Example of conditional logic in a style. - "color" => if model.count > 4 {"purple"} else {"gray"}; - // When passing numerical values to style!, "px" is implied. - "border" => "2px solid #004422"; "padding" => 20 - }, - // We can use normal Rust code and comments in the view. - h3![ format!("{} {}{} so far", model.count, model.what_we_count, plural) ], - button![ simple_ev(Ev::Click, Msg::Increment), "+" ], - button![ simple_ev(Ev::Click, Msg::Decrement), "-" ], - - // Optionally-displaying an element - if model.count >= 10 { h2![ style!{"padding" => 50}, "Nice!" ] } else { seed::empty() } - ], - success_level(model.count), // Incorporating a separate component - - h3![ "What precisely is it we're counting?" ], - input![ attrs!{At::Value => model.what_we_count}, input_ev(Ev::Input, Msg::ChangeWWC) ] + vec![ + div![ outer_style, + h1![ "The Grand Total" ], + div![ + style!{ + // Example of conditional logic in a style. + "color" => if model.count > 4 {"purple"} else {"gray"}; + // When passing numerical values to style!, "px" is implied. + "border" => "2px solid #004422"; "padding" => 20 + }, + // We can use normal Rust code and comments in the view. + h3![ format!("{} {}{} so far", model.count, model.what_we_count, plural) ], + button![ simple_ev(Ev::Click, Msg::Increment), "+" ], + button![ simple_ev(Ev::Click, Msg::Decrement), "-" ], + + // Optionally-displaying an element + if model.count >= 10 { h2![ style!{"padding" => 50}, "Nice!" ] } else { seed::empty() } + ], + success_level(model.count), // Incorporating a separate component + + h3![ "What precisely is it we're counting?" ], + input![ attrs!{At::Value => model.what_we_count}, input_ev(Ev::Input, Msg::ChangeWWC) ] + ] ] } diff --git a/examples/layered_structure/Cargo.toml b/examples/layered_structure/Cargo.toml deleted file mode 100644 index 60e1128ce..000000000 --- a/examples/layered_structure/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "layered" -version = "0.1.0" -authors = ["David O'Connor "] -edition = "2018" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -seed = {path = "../../"} -wasm-bindgen = "^0.2.37" -web-sys = "^0.3.6" diff --git a/examples/layered_structure/README.md b/examples/layered_structure/README.md deleted file mode 100644 index 630866e39..000000000 --- a/examples/layered_structure/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Demonstrates splitting model, message, and update into parts. - -WIP / not working or complete \ No newline at end of file diff --git a/examples/layered_structure/build.ps1 b/examples/layered_structure/build.ps1 deleted file mode 100644 index b46424b10..000000000 --- a/examples/layered_structure/build.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -cargo build --target wasm32-unknown-unknown -wasm-bindgen ../../target/wasm32-unknown-unknown/debug/layered.wasm --no-modules --out-dir ./pkg --out-name package - -#cargo build --target wasm32-unknown-unknown --release -#wasm-bindgen ../../target/wasm32-unknown-unknown/release/layered.wasm --no-modules --out-dir ./pkg --out-name package \ No newline at end of file diff --git a/examples/layered_structure/build.sh b/examples/layered_structure/build.sh deleted file mode 100644 index 4fb3a24d4..000000000 --- a/examples/layered_structure/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -cargo build --target wasm32-unknown-unknown -wasm-bindgen target/wasm32-unknown-unknown/debug/layered.wasm --no-modules --out-dir ./pkg --out-name package - - -#cargo build --target wasm32-unknown-unknown --release -#wasm-bindgen target/wasm32-unknown-unknown/release/layered.wasm --no-modules --out-dir ./pkg --out-name package \ No newline at end of file diff --git a/examples/layered_structure/index.html b/examples/layered_structure/index.html deleted file mode 100644 index cd68c1801..000000000 --- a/examples/layered_structure/index.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - Layered example - - - -
- - - - - - - diff --git a/examples/layered_structure/pkg/.keep b/examples/layered_structure/pkg/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/layered_structure/serve.py b/examples/layered_structure/serve.py deleted file mode 100644 index 545c1b1ab..000000000 --- a/examples/layered_structure/serve.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 - -import http.server -import os -import socketserver -import urllib - -PORT = 8000 - - -class Handler(http.server.SimpleHTTPRequestHandler): - # Allow SPA routing by redirecting subpaths. - def do_GET(self): - urlparts = urllib.parse.urlparse(self.path) - request_file_path = urlparts.path.strip('/') - if not os.path.exists(request_file_path): - self.path = '/' - - return http.server.SimpleHTTPRequestHandler.do_GET(self) - - -handler = Handler -# Add support for the WASM mime type. -handler.extensions_map.update({ - '.wasm': 'application/wasm', -}) - -socketserver.TCPServer.allow_reuse_address = True -with socketserver.TCPServer(("", PORT), handler) as httpd: - httpd.allow_reuse_address = True - print("Serving at port", PORT) - httpd.serve_forever() diff --git a/examples/layered_structure/src/lib.rs b/examples/layered_structure/src/lib.rs deleted file mode 100644 index c98474cdf..000000000 --- a/examples/layered_structure/src/lib.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! A simple, clichΓ© example demonstrating structure and syntax. - -#[macro_use] -extern crate seed; -use seed::prelude::*; - -enum TextBoxMessage { - -} - -struct TextBoxModel { - current_value: String, -} - -fn update_textbox(msg: TextBoxMsg, &mut model: TextBoxModel) -> Update { - model.current_value += " More text"; - Render(whatever) -} - -struct AppModel { - text_box1: TextBoxModel, - text_box2: TextBoxModel, -} - -enum AppMsg { - TextBox1Msg(TextBoxMsg), - TextBox2Msg(TextBoxMsg), -} - -fn update_app(msg: AppMsg, mut model: AppModel) -> Update -{ - ... -} - -/// A simple component. -fn success_level(clicks: i32) -> El { - let descrip = match clicks { - 0...5 => "Not very many πŸ™", - 6...9 => "I got my first real six-string 😐", - 10...11 => "Spinal Tap πŸ™‚", - _ => "Double pendulum πŸ™ƒ", - }; - p![descrip] -} - -/// The top-level component we pass to the virtual dom. Must accept the model as its -/// only argument, and output a single El. -fn view(model: &Model) -> El { - let plural = if model.count == 1 { "" } else { "s" }; - let text = format!("{} {}{} so far", model.count, model.what_we_count, plural); - - // Attrs, Style, Events, and children may be defined separately. - let outer_style = style! { - "display" => "flex"; - "flex-direction" => "column"; - "text-align" => "center" - }; - - div![ - outer_style, - h1!["The Grand Total"], - div![ - style! { - // Example of conditional logic in a style. - "color" => if model.count > 4 {"purple"} else {"gray"}; - // When passing numerical values to style!, "px" is implied. - "border" => "2px solid #004422"; "padding" => 20 - }, - // We can use normal Rust code and comments in the view. - h3![text, did_update(|_| log!("This shows when we increment"))], - button![simple_ev(Ev::Click, Msg::Increment), "+"], - button![simple_ev(Ev::Click, Msg::Decrement), "-"], - // Optionally-displaying an element, and lifecycle hooks - if model.count >= 10 { - h2![ - style! {"padding" => 50}, - "Nice!", - did_mount(|_| log!("This shows when clicks reach 10")), - will_unmount(|_| log!("This shows when clicks drop below 10")), - ] - } else { - seed::empty() - }, - ], - success_level(model.count), // Incorporating a separate component - h3!["What precisely is it we're counting?"], - input![ - attrs! {At::Value => model.what_we_count}, - input_ev(Ev::Input, Msg::ChangeWWC) - ], - ] -} - -#[wasm_bindgen] -pub fn render() { - seed::App::build(Model::default(), update, view) - .finish() - .run(); -} diff --git a/examples/todomvc/src/lib.rs b/examples/todomvc/src/lib.rs index 7874365cf..36545e536 100644 --- a/examples/todomvc/src/lib.rs +++ b/examples/todomvc/src/lib.rs @@ -312,8 +312,7 @@ fn view(model: &Model) -> Vec> { ul![class!["todo-list"], todo_els] ] } else { - // seed::empty() - span![] + seed::empty() }; vec![ @@ -335,8 +334,7 @@ fn view(model: &Model) -> Vec> { if model.active_count() > 0 || model.completed_count() > 0 { footer(&model) } else { - // seed::empty() - span![] + seed::empty() }, ] } diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml deleted file mode 100644 index 2d0fcb59f..000000000 --- a/proc_macros/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "proc_macros" -version = "0.1.0" -authors = ["DavidOConnor "] -edition = "2018" - -[lib] -proc-macro = true - -[dependencies] diff --git a/proc_macros/src/lib.rs b/proc_macros/src/lib.rs deleted file mode 100644 index c2ed99c7b..000000000 --- a/proc_macros/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -extern crate proc_macro; -use proc_macro::TokenStream; - -use syn; - -#[proc_macro] -pub fn update(item: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(item as syn::ItemFn); - let name = &input.ident; - let abi = &input.abi; -} diff --git a/src/lib.rs b/src/lib.rs index d55ab9ee1..bfc7d4798 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,8 +134,8 @@ pub mod tests { } } - fn view(_model: &Model) -> El { - div!["Hello world"] + fn view(_model: &Model) -> Vec> { + vec![div!["Hello world"]] } fn window_events(_model: &Model) -> Vec> { diff --git a/src/vdom.rs b/src/vdom.rs index 1918e377a..536704e34 100644 --- a/src/vdom.rs +++ b/src/vdom.rs @@ -179,6 +179,7 @@ pub struct AppData { pub routes: RefCell>>, window_listeners: RefCell>>, msg_listeners: RefCell>, + // mount_pt: RefCell } pub struct AppCfg { @@ -325,48 +326,41 @@ impl App { } } + pub fn setup_window_listeners(&self) { + if let Some(window_events) = self.cfg.window_events { + let mut new_listeners = (window_events)(&self.data.model.borrow()); + setup_window_listeners( + &util::window(), + &mut self.data.window_listeners.borrow_mut(), + &mut new_listeners, + &self.mailbox(), + ); + self.data.window_listeners.replace(new_listeners); + } + } + /// App initialization: Collect its fundamental components, setup, and perform /// an initial render. pub fn run(self) -> Self { // Our initial render. Can't initialize in new due to mailbox() requiring self. - // TODO: There's a lot of DRY between here and update. - let window = util::window(); + // "new" name is for consistency with update_inner. - let view_els = (self.cfg.view)(&self.data.model.borrow()); + let mut new = El::empty(dom_types::Tag::Section); + new.children = (self.cfg.view)(&self.data.model.borrow()); - // todo add these directly to the mount point. - let mut topel_vdom = El::empty(dom_types::Tag::Section); - topel_vdom.children = view_els; - - // TODO: use window events - if self.cfg.window_events.is_some() { - setup_window_listeners( - &util::window(), - &mut Vec::new(), - // TODO: - // Fix this. Bug where if we try to add initial listeners, - // we get many runtime panics. Workaround is to wait until - // app.update, which means an event must be triggered - // prior to window listeners working. - &mut Vec::new(), - // &mut (window_events)(model), - &self.mailbox(), - ); - } + self.setup_window_listeners(); - let document = window.document().expect("Problem getting document"); - setup_input_listeners(&mut topel_vdom); - setup_websys_el_and_children(&document, &mut topel_vdom); + setup_input_listeners(&mut new); + setup_websys_el_and_children(&util::document(), &mut new); - attach_listeners(&mut topel_vdom, &self.mailbox()); + attach_listeners(&mut new, &self.mailbox()); // Attach all top-level elements to the mount point: This is where our initial render occurs. - websys_bridge::attach_el_and_children(&mut topel_vdom, &self.cfg.mount_point, &self); - // for top_child in &mut topel_vdom.children { - // websys_bridge::attach_el_and_children(top_child, &self.cfg.mount_point, &self) - // } + for top_child in &mut new.children { + websys_bridge::attach_el_and_children(top_child, &self.cfg.mount_point, &self) + } - self.data.main_el_vdom.replace(Some(topel_vdom)); + self.data.main_el_vdom.replace(Some(new)); let self_for_closure = self.clone(); let self_for_closure2 = self.clone(); @@ -418,31 +412,15 @@ impl App { effect, } = (self.cfg.update)(message, &mut self.data.model.borrow_mut()); - if let Some(window_events) = self.cfg.window_events { - let mut new_listeners = (window_events)(&self.data.model.borrow()); - setup_window_listeners( - &util::window(), - &mut self.data.window_listeners.borrow_mut(), - // &mut Vec::new(), - &mut new_listeners, - &self.mailbox(), - ); - self.data.window_listeners.replace(new_listeners); - } + self.setup_window_listeners(); if should_render == ShouldRender::Render { // Create a new vdom: The top element, and all its children. Does not yet // have associated web_sys elements. + let mut new = El::empty(dom_types::Tag::Section); + new.children = (self.cfg.view)(&self.data.model.borrow());; - let view_els = (self.cfg.view)(&self.data.model.borrow()); - - // todo add these directly to the mount point. - let mut topel_new_vdom = El::empty(dom_types::Tag::Section); - topel_new_vdom.children = view_els; - - // self.data.main_el_vdom.children = view_els; - - let mut old_vdom = self + let mut old = self .data .main_el_vdom .borrow_mut() @@ -451,22 +429,41 @@ impl App { // Detach all old listeners before patching. We'll re-add them as required during patching. // We'll get a runtime panic if any are left un-removed. - detach_listeners(&mut old_vdom); - - patch( - &self.cfg.document, - old_vdom, - &mut topel_new_vdom, - &self.cfg.mount_point, - // &self.cfg.document, - None, - &self.mailbox(), - &self.clone(), - ); + detach_listeners(&mut old); + + // todo copied from children loop in patch fn (DRY) + let num_children_in_both = old.children.len().min(new.children.len()); + let mut old_children_iter = old.children.into_iter(); + let mut new_children_iter = new.children.iter_mut(); + + let mut last_visited_node: Option = None; + // + // if let Some(update_actions) = &mut placeholder_topel.hooks.did_update { + // (update_actions.actions)(&old_el_ws) // todo put in / back + // } + + for _i in 0..num_children_in_both { + let child_old = old_children_iter.next().unwrap(); + let child_new = new_children_iter.next().unwrap(); + + patch( + &self.cfg.document, + child_old, + child_new, + &self.cfg.mount_point, + // match last_visited_node.as_ref() { + // Some(node) => node.next_sibling(), + // None => old_el_ws.first_child(), + // }, + None, // todo make it the next item in new? + &self.mailbox(), + &self.clone(), + ); + } // Now that we've re-rendered, replace our stored El with the new one; // it will be used as the old El next time. - self.data.main_el_vdom.borrow_mut().replace(topel_new_vdom); + self.data.main_el_vdom.borrow_mut().replace(new); } if let Some(effect) = effect { diff --git a/src/websys_bridge.rs b/src/websys_bridge.rs index 6ba5d3aa9..5ef43092b 100644 --- a/src/websys_bridge.rs +++ b/src/websys_bridge.rs @@ -5,23 +5,6 @@ use crate::dom_types; use crate::dom_types::El; use crate::vdom::App; -/// Reduces DRY -/// todo can't find a suitable trait for this. Seems like set_autofocus is -/// implemented individually for each of these el types. -//fn autofocus_helper(el: E) { -// match val { -// "true" => { -// el.set_autofocus(true); -// set_special = true; -// }, -// "false" => { -// el.set_autofocus(false); -// set_special = true; -// }, -// _ => () -// } -//} - /// Add a shim to make check logic more natural than the DOM handles it. fn set_attr_shim(el_ws: &web_sys::Node, at: &dom_types::At, val: &str) { let mut set_special = false;