diff --git a/Cargo.toml b/Cargo.toml index 97b0fabafaa..b97e16bc87b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ version = "0.1.0" authors = ["Denis Kolodin "] [dependencies] -stdweb = { git = "https://github.com/koute/stdweb" } +stdweb = { git = "https://github.com/DenisKolodin/stdweb", branch = "pub-textnode" } +#stdweb = { git = "https://github.com/koute/stdweb" } diff --git a/examples/todomvc/Cargo.lock b/examples/todomvc/Cargo.lock index 5562944149a..570427b59f6 100644 --- a/examples/todomvc/Cargo.lock +++ b/examples/todomvc/Cargo.lock @@ -32,7 +32,7 @@ dependencies = [ [[package]] name = "stdweb" version = "0.2.0" -source = "git+https://github.com/koute/stdweb#9ba27f1c317db65049f675834485d3d12405e1f3" +source = "git+https://github.com/DenisKolodin/stdweb?branch=pub-textnode#6c0e2b7b096051865b1bc7975b42258d94518d45" dependencies = [ "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -49,7 +49,7 @@ dependencies = [ name = "yew" version = "0.1.0" dependencies = [ - "stdweb 0.2.0 (git+https://github.com/koute/stdweb)", + "stdweb 0.2.0 (git+https://github.com/DenisKolodin/stdweb?branch=pub-textnode)", ] [metadata] @@ -58,4 +58,4 @@ dependencies = [ "checksum num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "cacfcab5eb48250ee7d0c7896b51a2c5eec99c1feea5f32025635f5ae4b00070" "checksum serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1c57ab4ec5fa85d08aaf8ed9245899d9bbdd66768945b21113b84d5f595cb6a1" "checksum serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7cf5b0b5b4bd22eeecb7e01ac2e1225c7ef5e4272b79ee28a8392a8c8489c839" -"checksum stdweb 0.2.0 (git+https://github.com/koute/stdweb)" = "" +"checksum stdweb 0.2.0 (git+https://github.com/DenisKolodin/stdweb?branch=pub-textnode)" = "" diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs index 729934c1e36..7e152cbd063 100644 --- a/examples/todomvc/src/main.rs +++ b/examples/todomvc/src/main.rs @@ -59,6 +59,7 @@ fn update(model: &mut Model, msg: Msg) { completed: false, }; model.entries.push(entry); + model.value = "".to_string(); } Msg::Update(val) => { println!("Input: {}", val); @@ -70,8 +71,7 @@ fn update(model: &mut Model, msg: Msg) { Msg::SetFilter(filter) => { model.filter = filter; } - Msg::Nope => { - } + Msg::Nope => {} } } diff --git a/src/html.rs b/src/html.rs index 44c113702e2..73508c7fb82 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,22 +1,36 @@ use std::fmt; use std::rc::Rc; use std::cell::RefCell; -use std::collections::HashMap; - -/* -pub trait Message {} - -impl Fn(T) -> Self for Message { -} -*/ +use std::collections::{HashMap, HashSet}; use stdweb; -use stdweb::web::{INode, IElement, Element, document}; +use stdweb::web::{INode, IElement, Node, Element, TextNode, EventListenerHandle, document}; use stdweb::web::event::{IMouseEvent, IKeyboardEvent}; use stdweb::web::html_element::InputElement; use stdweb::unstable::TryInto; +macro_rules! debug { + ($($e:expr),*) => { + if cfg!(debug) { + println!($($e,)*); + } + }; +} + +macro_rules! warn { + ($($e:expr),*) => { + eprintln!($($e,)*); + }; +} + +fn clear_body() { + let body = document().query_selector("body").unwrap(); + while body.has_child_nodes() { + body.remove_child(&body.last_child().unwrap()).unwrap(); + } +} + pub fn program(mut model: M, update: U, view: V) where M: 'static, @@ -25,19 +39,24 @@ where V: Fn(&M) -> Html + 'static, { stdweb::initialize(); + clear_body(); + let body = document().query_selector("body").unwrap(); // No messages at start let messages = Rc::new(RefCell::new(Vec::new())); + let mut last_frame = VNode::from(view(&model)); + last_frame.apply(&body, None, messages.clone()); + let mut last_frame = Some(last_frame); + let mut callback = move || { + debug!("Yew Loop Callback"); let mut borrowed = messages.borrow_mut(); for msg in borrowed.drain(..) { update(&mut model, msg); } - let html = view(&model); - let body = document().query_selector("body").unwrap(); - while body.has_child_nodes() { - body.remove_child(&body.last_child().unwrap()).unwrap(); - } - html.render(messages.clone(), &body); + let mut next_frame = VNode::from(view(&model)); + debug!("Do apply"); + next_frame.apply(&body, last_frame.take(), messages.clone()); + last_frame = Some(next_frame); }; // Initial call for first rendering callback(); @@ -50,11 +69,11 @@ where stdweb::event_loop(); } -pub type Html = VNode; +pub type Html = VTag; pub trait Listener { fn kind(&self) -> &'static str; - fn attach(&mut self, element: &Element, messages: Messages); + fn attach(&mut self, element: &Element, messages: Messages) -> EventListenerHandle; } impl fmt::Debug for Listener { @@ -66,52 +85,171 @@ impl fmt::Debug for Listener { type Messages = Rc>>; type Listeners = Vec>>; type Attributes = HashMap<&'static str, String>; -type Classes = Vec<&'static str>; - -trait Render { - fn render(self, messages: Messages, element: &Element); -} - -pub enum Child { - VNode(VNode), - VText(VText), +type Classes = HashSet<&'static str>; + +/// Bind virtual element to a DOM reference. +pub enum VNode { + VTag { + reference: Option, + vtag: VTag, + }, + VText { + reference: Option, // TODO Replace with TextNode + vtext: VText, + }, } -impl From for Child { +impl From for VNode { fn from(value: T) -> Self { - Child::VText(VText::new(value)) + VNode::VText { + reference: None, + vtext: VText::new(value), + } } } -impl fmt::Debug for Child { +impl fmt::Debug for VNode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - &Child::VNode(ref vnode) => vnode.fmt(f), - &Child::VText(ref vtext) => vtext.fmt(f), + &VNode::VTag { ref vtag, .. } => vtag.fmt(f), + &VNode::VText { ref vtext, .. } => vtext.fmt(f), } } } +impl VNode { + fn remove(self, parent: &T) { + let opt_ref: Option = { + match self { + VNode::VTag { reference, .. } => reference.map(Node::from), + VNode::VText { reference, .. } => reference.map(Node::from), + } + }; + if let Some(node) = opt_ref { + if let Err(_) = parent.remove_child(&node) { + warn!("Node not found to remove: {:?}", node); + } + } + } -impl Render for Child { - fn render(self, messages: Messages, element: &Element) { - match self { - Child::VNode(vnode) => vnode.render(messages, element), - Child::VText(vtext) => vtext.render(messages, element), + fn apply(&mut self, parent: &T, last: Option>, messages: Messages) { + match *self { + VNode::VTag { + ref mut vtag, + ref mut reference, + } => { + let left = vtag; + let mut right = None; + match last { + Some(VNode::VTag { + mut vtag, + reference: Some(mut element), + }) => { + // Copy reference from right to left (as is) + right = Some(vtag); + *reference = Some(element); + } + Some(VNode::VText { reference: Some(wrong), .. }) => { + let mut element = document().create_element(left.tag); + parent.replace_child(&element, &wrong); + *reference = Some(element); + } + Some(VNode::VTag { reference: None, .. }) | + Some(VNode::VText { reference: None, .. }) | + None => { + let mut element = document().create_element(left.tag); + parent.append_child(&element); + *reference = Some(element); + } + } + let element_mut = reference.as_mut().expect("vtag must be here"); + // Update parameters + let mut rights = { + if let Some(ref mut right) = right { + right.childs.drain(..).map(Some).collect::>() + } else { + Vec::new() + } + }; + // TODO Consider to use: &mut Messages here; + left.render(element_mut, right, messages.clone()); + let mut lefts = left.childs.iter_mut().map(Some).collect::>(); + // Process children + let diff = lefts.len() as i32 - rights.len() as i32; + if diff > 0 { + for _ in 0..diff { + rights.push(None); + } + } else if diff < 0 { + for _ in 0..-diff { + lefts.push(None); + } + } + for pair in lefts.into_iter().zip(rights) { + match pair { + (Some(left), right) => { + left.apply(element_mut, right, messages.clone()); + } + (None, Some(right)) => { + right.remove(element_mut); + } + (None, None) => { + panic!("redundant iterations during diff"); + } + } + } + //vtag.apply(parent, reference, last, messages); + } + VNode::VText { + ref mut vtext, + ref mut reference, + } => { + let left = vtext; + let mut right = None; + match last { + Some(VNode::VText { + mut vtext, + reference: Some(mut element), + }) => { + right = Some(vtext); + *reference = Some(element); + } + Some(VNode::VTag { reference: Some(wrong), .. }) => { + let mut element = document().create_text_node(&left.text); + parent.replace_child(&element, &wrong); + *reference = Some(element); + } + Some(VNode::VTag { reference: None, .. }) | + Some(VNode::VText { reference: None, .. }) | + None => { + let element = document().create_text_node(&left.text); + parent.append_child(&element); + *reference = Some(element); + } + } + let element_mut = reference.as_mut().expect("vtext must be here"); + left.render(element_mut, right); + } } } } -impl From for Child { +impl From for VNode { fn from(vtext: VText) -> Self { - Child::VText(vtext) + VNode::VText { + reference: None, + vtext, + } } } -impl From> for Child { - fn from(vnode: VNode) -> Self { - Child::VNode(vnode) +impl From> for VNode { + fn from(vtag: VTag) -> Self { + VNode::VTag { + reference: None, + vtag, + } } } @@ -129,40 +267,52 @@ impl VText { pub fn new(text: T) -> Self { VText { text: text.to_string() } } -} -impl Render for VText { - fn render(self, _: Messages, element: &Element) { - let child_element = document().create_text_node(&self.text); - element.append_child(&child_element); + fn render(&mut self, subject: &TextNode, opposite: Option) { + if let Some(opposite) = opposite { + if self.text != opposite.text { + subject.set_node_value(Some(&self.text)); + } + } else { + subject.set_node_value(Some(&self.text)); + } } } - -pub struct VNode { +pub struct VTag { tag: &'static str, listeners: Listeners, + captured: Vec, attributes: Attributes, - childs: Vec>, + childs: Vec>, classes: Classes, value: Option, + kind: Option, } -impl fmt::Debug for VNode { +impl fmt::Debug for VTag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "VNode {{ tag: {} }}", self.tag) + write!(f, "VTag {{ tag: {} }}", self.tag) } } -impl VNode { +enum Patch { + Add(ID, T), + Replace(ID, T), + Remove(ID), +} + +impl VTag { pub fn new(tag: &'static str) -> Self { - VNode { + VTag { tag: tag, - classes: Vec::new(), + classes: Classes::new(), attributes: HashMap::new(), listeners: Vec::new(), + captured: Vec::new(), childs: Vec::new(), value: None, + kind: None, } } @@ -170,18 +320,22 @@ impl VNode { self.tag } - pub fn add_child(&mut self, child: Child) { + pub fn add_child(&mut self, child: VNode) { self.childs.push(child); } pub fn add_classes(&mut self, class: &'static str) { - self.classes.push(class); + self.classes.insert(class); } pub fn set_value(&mut self, value: &T) { self.value = Some(value.to_string()); } + pub fn set_kind(&mut self, value: T) { + self.kind = Some(value.to_string()); + } + pub fn add_attribute(&mut self, name: &'static str, value: T) { self.attributes.insert(name, value.to_string()); } @@ -189,37 +343,173 @@ impl VNode { pub fn add_listener(&mut self, listener: Box>) { self.listeners.push(listener); } -} -impl Render for VNode { - fn render(mut self, messages: Messages, element: &Element) { - let child_element = document().create_element(self.tag); - let child_element = { - let cloned: Result = child_element.clone().try_into(); - if let &Some(ref value) = &self.value { - if let Ok(input_element) = cloned { - input_element.set_value(value); - input_element.into() + fn soakup_classes(&mut self, ancestor: &mut Option) -> Vec> { + let mut changes = Vec::new(); + if let &mut Some(ref ancestor) = ancestor { + let to_add = self.classes.difference(&ancestor.classes).map(|class| { + Patch::Add(*class, ()) + }); + changes.extend(to_add); + let to_remove = ancestor.classes.difference(&self.classes).map(|class| { + Patch::Remove(*class) + }); + changes.extend(to_remove); + } else { + // Add everything + let to_add = self.classes.iter().map(|class| Patch::Add(*class, ())); + changes.extend(to_add); + } + changes + } + + fn soakup_attributes(&mut self, ancestor: &mut Option) -> Vec> { + let mut changes = Vec::new(); + if let &mut Some(ref mut ancestor) = ancestor { + let left_keys = self.attributes.keys().collect::>(); + let right_keys = ancestor.attributes.keys().collect::>(); + let to_add = left_keys.difference(&right_keys).map(|key| { + let value = self.attributes.get(*key).unwrap(); + Patch::Add(key.to_string(), value.to_string()) + }); + changes.extend(to_add); + for key in left_keys.intersection(&right_keys) { + let left_value = self.attributes.get(*key).unwrap(); + let right_value = ancestor.attributes.get(*key).unwrap(); + if left_value != right_value { + let mutator = Patch::Replace(key.to_string(), left_value.to_string()); + changes.push(mutator); + } + } + let to_remove = right_keys.difference(&left_keys).map(|key| { + Patch::Remove(key.to_string()) + }); + changes.extend(to_remove); + } else { + for (key, value) in self.attributes.iter() { + let mutator = Patch::Add(key.to_string(), value.to_string()); + changes.push(mutator); + } + } + changes + } + + fn soakup_kind(&mut self, ancestor: &mut Option) -> Option> { + match ( + &self.kind, + ancestor.as_mut().and_then(|anc| anc.kind.take()), + ) { + (&Some(ref left), Some(ref right)) => { + if left != right { + Some(Patch::Replace(left.to_string(), ())) } else { - child_element + None + } + } + (&Some(ref left), None) => Some(Patch::Add(left.to_string(), ())), + (&None, Some(right)) => Some(Patch::Remove(right)), + (&None, None) => None, + } + } + + fn soakup_value(&mut self, ancestor: &mut Option) -> Option> { + match ( + &self.value, + ancestor.as_mut().and_then(|anc| anc.value.take()), + ) { + (&Some(ref left), Some(ref right)) => { + if left != right { + Some(Patch::Replace(left.to_string(), ())) + } else { + None + } + } + (&Some(ref left), None) => Some(Patch::Add(left.to_string(), ())), + (&None, Some(right)) => Some(Patch::Remove(right)), + (&None, None) => None, + } + } +} + +impl VTag { + fn render(&mut self, subject: &Element, mut opposite: Option, messages: Messages) { + // TODO Replace self if tagName differs + + let changes = self.soakup_classes(&mut opposite); + for change in changes { + let list = subject.class_list(); + match change { + Patch::Add(class, _) | + Patch::Replace(class, _) => { + list.add(&class); + } + Patch::Remove(class) => { + list.remove(&class); + } + } + } + + let changes = self.soakup_attributes(&mut opposite); + for change in changes { + match change { + Patch::Add(key, value) | + Patch::Replace(key, value) => { + set_attribute(&subject, &key, &value); + } + Patch::Remove(key) => { + remove_attribute(&subject, &key); + } + } + } + + if let Some(change) = self.soakup_kind(&mut opposite) { + let input: Result = subject.clone().try_into(); + if let Ok(input) = input { + match change { + Patch::Add(kind, _) | + Patch::Replace(kind, _) => { + input.set_kind(&kind); + } + Patch::Remove(_) => { + input.set_kind(""); + } } } else { - child_element + panic!("tried to set `type` kind for non input element"); } - }; - for (name, value) in self.attributes { - set_attribute(&child_element, name, &value); } - for class in self.classes { - child_element.class_list().add(&class); + + if let Some(change) = self.soakup_value(&mut opposite) { + let input: Result = subject.clone().try_into(); + if let Ok(input) = input { + match change { + Patch::Add(kind, _) | + Patch::Replace(kind, _) => { + input.set_value(&kind); + } + Patch::Remove(_) => { + input.set_value(""); + } + } + } else { + panic!("tried to set `value` kind for non input element"); + } } - for mut listener in self.listeners.drain(..) { - listener.attach(&child_element, messages.clone()); + + // Every render it removes all listeners and attach it back later + // TODO Compare references of handler to do listeners update better + if let Some(mut opposite) = opposite { + for mut handle in opposite.captured.drain(..) { + debug!("Removing handler..."); + handle.remove(); + } } - for child in self.childs.drain(..) { - child.render(messages.clone(), &child_element); + + for mut listener in self.listeners.drain(..) { + debug!("Add listener..."); + let handle = listener.attach(&subject, messages.clone()); + self.captured.push(handle); } - element.append_child(&child_element); } } @@ -253,10 +543,12 @@ macro_rules! impl_action { stringify!($action) } - fn attach(&mut self, element: &Element, messages: Messages) { + fn attach(&mut self, element: &Element, messages: Messages) + -> EventListenerHandle { let handler = self.0.take().unwrap(); let this = element.clone(); let sender = move |event: $type| { + debug!("Event handler: {}", stringify!($type)); event.stop_propagation(); let handy_event: $ret = $convert(&this, event); let msg = handler(handy_event); @@ -269,7 +561,7 @@ macro_rules! impl_action { setTimeout(yew_loop); } }; - element.add_event_listener(sender); + element.add_event_listener(sender) } } } @@ -326,9 +618,7 @@ pub struct KeyData { impl From for KeyData { fn from(event: T) -> Self { - KeyData { - key: event.key(), - } + KeyData { key: event.key() } } } @@ -338,3 +628,6 @@ fn set_attribute(element: &Element, name: &str, value: &str) { js!( @{element}.setAttribute( @{name}, @{value} ); ); } +fn remove_attribute(element: &Element, name: &str) { + js!( @{element}.removeAttribute( @{name} ); ); +} diff --git a/src/macros.rs b/src/macros.rs index 367c2318b24..8c78dfa90c5 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,10 +1,10 @@ -use html::{VNode, Child, Listener}; +use html::{VTag, VNode, Listener}; #[macro_export] macro_rules! html_impl { // Start of openging tag ($stack:ident (< $starttag:ident $($tail:tt)*)) => { - let node = $crate::html::VNode::new(stringify!($starttag)); + let node = $crate::html::VTag::new(stringify!($starttag)); $stack.push(node); html_impl! { $stack ($($tail)*) } }; @@ -18,6 +18,12 @@ macro_rules! html_impl { $crate::macros::set_value(&mut $stack, $value); html_impl! { $stack ($($tail)*) } }; + // PATTERN: attribute=value, - workaround for `type` attribute + // because `type` is a keyword in Rust + ($stack:ident (type = $kind:expr, $($tail:tt)*)) => { + $crate::macros::set_kind(&mut $stack, $kind); + html_impl! { $stack ($($tail)*) } + }; // Events: ($stack:ident (onclick = $handler:expr, $($tail:tt)*)) => { html_impl! { $stack ((onclick) = $handler, $($tail)*) } @@ -39,12 +45,6 @@ macro_rules! html_impl { $crate::macros::attach_listener(&mut $stack, Box::new(listener)); html_impl! { $stack ($($tail)*) } }; - // PATTERN: attribute=value, - workaround for `type` attribute - // because `type` is a keyword in Rust - ($stack:ident (type = $val:expr, $($tail:tt)*)) => { - $crate::macros::add_attribute(&mut $stack, "type", $val); - html_impl! { $stack ($($tail)*) } - }; ($stack:ident ($attr:ident = $val:expr, $($tail:tt)*)) => { $crate::macros::add_attribute(&mut $stack, stringify!($attr), $val); html_impl! { $stack ($($tail)*) } @@ -52,14 +52,14 @@ macro_rules! html_impl { // PATTERN: { for expression } ($stack:ident ({ for $eval:expr } $($tail:tt)*)) => { let nodes = $eval; - for node in nodes.map($crate::html::Child::from) { + for node in nodes.map($crate::html::VNode::from) { $crate::macros::add_child(&mut $stack, node); } html_impl! { $stack ($($tail)*) } }; // PATTERN: { expression } ($stack:ident ({ $eval:expr } $($tail:tt)*)) => { - let node = $crate::html::Child::from($eval); + let node = $crate::html::VNode::from($eval); $crate::macros::add_child(&mut $stack, node); html_impl! { $stack ($($tail)*) } }; @@ -93,10 +93,10 @@ macro_rules! html { }; } -type Stack = Vec>; +type Stack = Vec>; #[doc(hidden)] -pub fn unpack(mut stack: Stack) -> VNode { +pub fn unpack(mut stack: Stack) -> VTag { if stack.len() != 1 { panic!("exactly one element have to be in html!"); } @@ -112,6 +112,15 @@ pub fn set_value(stack: &mut Stack, value: &T) { } } +#[doc(hidden)] +pub fn set_kind(stack: &mut Stack, value: T) { + if let Some(node) = stack.last_mut() { + node.set_kind(value); + } else { + panic!("no tag to set type: {}", value.to_string()); + } +} + #[doc(hidden)] pub fn add_attribute(stack: &mut Stack, name: &'static str, value: T) { if let Some(node) = stack.last_mut() { @@ -140,7 +149,7 @@ pub fn attach_listener(stack: &mut Stack, listener: Box> } #[doc(hidden)] -pub fn add_child(stack: &mut Stack, child: Child) { +pub fn add_child(stack: &mut Stack, child: VNode) { if let Some(parent) = stack.last_mut() { parent.add_child(child); } else { @@ -158,7 +167,7 @@ pub fn child_to_parent(stack: &mut Stack, endtag: Option<&'static str> } } if !stack.is_empty() { - stack.last_mut().unwrap().add_child(Child::VNode(node)); + stack.last_mut().unwrap().add_child(VNode::from(node)); } else { // Keep the last node in the stack stack.push(node);