//! The virtual_node module exposes the `VirtualNode` struct and methods that power our //! virtual dom. // TODO: A few of these dependencies (including js_sys) are used to power events.. yet events // only work on wasm32 targest. So we should start sprinkling some // // #[cfg(target_arch = "wasm32")] // #[cfg(not(target_arch = "wasm32"))] // // Around in order to get rid of dependencies that we don't need in non wasm32 targets use std::collections::HashMap; use std::fmt; use std::rc::Rc; pub mod virtual_node_test_utils; use web_sys::{self, Element, EventTarget, Node, Text}; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use std::ops::Deref; use std::sync::Mutex; // Used to uniquely identify elements that contain closures so that the DomUpdater can // look them up by their unique id. // When the DomUpdater sees that the element no longer exists it will drop all of it's // Rc'd Closures for those events. use lazy_static::lazy_static; lazy_static! { static ref ELEM_UNIQUE_ID: Mutex = Mutex::new(0); } /// When building your views you'll typically use the `html!` macro to generate /// `VirtualNode`'s. /// /// `html! {
}` really generates a `VirtualNode` with /// one child (span). /// /// Later, on the client side, you'll use the `diff` and `patch` modules to /// update the real DOM with your latest tree of virtual nodes (virtual dom). /// /// Or on the server side you'll just call `.to_string()` on your root virtual node /// in order to recursively render the node and all of its children. /// /// TODO: Make all of these fields private and create accessor methods /// TODO: Create a builder to create instances of VirtualNode::Element with /// attrs and children without having to explicitly create a VElement #[derive(PartialEq)] pub enum VirtualNode { /// An element node (node type `ELEMENT_NODE`). Element(VElement), /// A text node (node type `TEXT_NODE`). /// /// Note: This wraps a `VText` instead of a plain `String` in /// order to enable custom methods like `create_text_node()` on the /// wrapped type. Text(VText), /// A User-defined componen node (node type COMPONENT_NODE) Component(VComponent), } #[derive(PartialEq)] pub struct VElement { /// The HTML tag, such as "div" pub tag: String, /// HTML attributes such as id, class, style, etc pub attrs: HashMap, /// Events that will get added to your real DOM element via `.addEventListener` pub events: Events, /// The children of this `VirtualNode`. So a
structure would /// have a parent div and one child, em. pub children: Vec, } #[derive(PartialEq)] pub struct VText { pub text: String, } #[derive(PartialEq)] pub struct VComponent {} impl VirtualNode { /// Create a new virtual element node with a given tag. /// /// These get patched into the DOM using `document.createElement` /// /// ```ignore /// use virtual_dom_rs::VirtualNode; /// /// let div = VirtualNode::element("div"); /// ``` pub fn element(tag: S) -> Self where S: Into, { VirtualNode::Element(VElement::new(tag)) } /// Create a new virtual text node with the given text. /// /// These get patched into the DOM using `document.createTextNode` /// /// ```ignore /// use virtual_dom_rs::VirtualNode; /// /// let div = VirtualNode::text("div"); /// ``` pub fn text(text: S) -> Self where S: Into, { VirtualNode::Text(VText::new(text.into())) } /// Return a [`VElement`] reference, if this is an [`Element`] variant. /// /// [`VElement`]: struct.VElement.html /// [`Element`]: enum.VirtualNode.html#variant.Element pub fn as_velement_ref(&self) -> Option<&VElement> { match self { VirtualNode::Element(ref element_node) => Some(element_node), _ => None, } } /// Return a mutable [`VElement`] reference, if this is an [`Element`] variant. /// /// [`VElement`]: struct.VElement.html /// [`Element`]: enum.VirtualNode.html#variant.Element pub fn as_velement_mut(&mut self) -> Option<&mut VElement> { match self { VirtualNode::Element(ref mut element_node) => Some(element_node), _ => None, } } /// Return a [`VText`] reference, if this is an [`Text`] variant. /// /// [`VText`]: struct.VText.html /// [`Text`]: enum.VirtualNode.html#variant.Text pub fn as_vtext_ref(&self) -> Option<&VText> { match self { VirtualNode::Text(ref text_node) => Some(text_node), _ => None, } } /// Return a mutable [`VText`] reference, if this is an [`Text`] variant. /// /// [`VText`]: struct.VText.html /// [`Text`]: enum.VirtualNode.html#variant.Text pub fn as_vtext_mut(&mut self) -> Option<&mut VText> { match self { VirtualNode::Text(ref mut text_node) => Some(text_node), _ => None, } } /// Create and return a `CreatedNode` instance (containing a DOM `Node` /// together with potentially related closures) for this virtual node. pub fn create_dom_node(&self) -> CreatedNode { match self { VirtualNode::Text(text_node) => { CreatedNode::without_closures(text_node.create_text_node()) } VirtualNode::Element(element_node) => element_node.create_element_node().into(), VirtualNode::Component(_) => todo!("WIP on Component Syntax"), } } /// Used by html-macro to insert space before text that is inside of a block that came after /// an open tag. /// /// html! {
{world}
} /// /// So that we end up with
world
when we're finished parsing. pub fn insert_space_before_text(&mut self) { match self { VirtualNode::Text(text_node) => { text_node.text = " ".to_string() + &text_node.text; } _ => {} } } /// Used by html-macro to insert space after braced text if we know that the next block is /// another block or a closing tag. /// /// html! {
{Hello} {world}
} ->
Hello world
/// html! {
{Hello}
} ->
Hello
/// /// So that we end up with
Hello world
when we're finished parsing. pub fn insert_space_after_text(&mut self) { match self { VirtualNode::Text(text_node) => { text_node.text += " "; } _ => {} } } } impl VElement { pub fn new(tag: S) -> Self where S: Into, { VElement { tag: tag.into(), attrs: HashMap::new(), events: Events(HashMap::new()), children: vec![], } } /// Build a DOM element by recursively creating DOM nodes for this element and it's /// children, it's children's children, etc. pub fn create_element_node(&self) -> CreatedNode { let document = web_sys::window().unwrap().document().unwrap(); let element = if html_validation::is_svg_namespace(&self.tag) { document .create_element_ns(Some("http://www.w3.org/2000/svg"), &self.tag) .unwrap() } else { document.create_element(&self.tag).unwrap() }; let mut closures = HashMap::new(); self.attrs.iter().for_each(|(name, value)| { if name == "unsafe_inner_html" { element.set_inner_html(value); return; } element .set_attribute(name, value) .expect("Set element attribute in create element"); }); if self.events.0.len() > 0 { let unique_id = create_unique_identifier(); element .set_attribute("data-vdom-id".into(), &unique_id.to_string()) .expect("Could not set attribute on element"); closures.insert(unique_id, vec![]); self.events.0.iter().for_each(|(onevent, callback)| { // onclick -> click let event = &onevent[2..]; let current_elem: &EventTarget = element.dyn_ref().unwrap(); current_elem .add_event_listener_with_callback( event, callback.as_ref().as_ref().unchecked_ref(), ) .unwrap(); closures .get_mut(&unique_id) .unwrap() .push(Rc::clone(callback)); }); } let mut previous_node_was_text = false; self.children.iter().for_each(|child| { match child { VirtualNode::Text(text_node) => { let current_node = element.as_ref() as &web_sys::Node; // We ensure that the text siblings are patched by preventing the browser from merging // neighboring text nodes. Originally inspired by some of React's work from 2016. // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes // -> https://github.com/facebook/react/pull/5753 // // `ptns` = Percy text node separator if previous_node_was_text { let separator = document.create_comment("ptns"); current_node .append_child(separator.as_ref() as &web_sys::Node) .unwrap(); } current_node .append_child(&text_node.create_text_node()) .unwrap(); previous_node_was_text = true; } VirtualNode::Element(element_node) => { previous_node_was_text = false; let child = element_node.create_element_node(); let child_elem: Element = child.node; closures.extend(child.closures); element.append_child(&child_elem).unwrap(); } VirtualNode::Component(_) => { todo!("WIP on Component Syntax") } } }); if let Some(on_create_elem) = self.events.0.get("on_create_elem") { let on_create_elem: &js_sys::Function = on_create_elem.as_ref().as_ref().unchecked_ref(); on_create_elem .call1(&wasm_bindgen::JsValue::NULL, &element) .unwrap(); } CreatedNode { node: element, closures, } } } impl VText { /// Create an new `VText` instance with the specified text. pub fn new(text: S) -> Self where S: Into, { VText { text: text.into() } } /// Return a `Text` element from a `VirtualNode`, typically right before adding it /// into the DOM. pub fn create_text_node(&self) -> Text { let document = web_sys::window().unwrap().document().unwrap(); document.create_text_node(&self.text) } } /// A node along with all of the closures that were created for that /// node's events and all of it's child node's events. pub struct CreatedNode { /// A `Node` or `Element` that was created from a `VirtualNode` pub node: T, /// A map of a node's unique identifier along with all of the Closures for that node. /// /// The DomUpdater uses this to look up nodes and see if they're still in the page. If not /// the reference that we maintain to their closure will be dropped, thus freeing the Closure's /// memory. pub closures: HashMap>, } impl CreatedNode { pub fn without_closures>(node: N) -> Self { CreatedNode { node: node.into(), closures: HashMap::with_capacity(0), } } } impl Deref for CreatedNode { type Target = T; fn deref(&self) -> &Self::Target { &self.node } } impl From> for CreatedNode { fn from(other: CreatedNode) -> CreatedNode { CreatedNode { node: other.node.into(), closures: other.closures, } } } fn create_unique_identifier() -> u32 { let mut elem_unique_id = ELEM_UNIQUE_ID.lock().unwrap(); *elem_unique_id += 1; *elem_unique_id } /// A trait with common functionality for rendering front-end views. pub trait View { /// Render a VirtualNode, or any IntoIter fn render(&self) -> VirtualNode; } impl From<&V> for VirtualNode where V: View, { fn from(v: &V) -> Self { v.render() } } /// Used by the html! macro for all braced child nodes so that we can use any type /// that implements Into /// /// html! {
{ nodes }
} /// /// nodes can be a String .. VirtualNode .. Vec ... etc pub struct IterableNodes(Vec); impl IterableNodes { /// Retrieve the first node mutably pub fn first(&mut self) -> &mut VirtualNode { self.0.first_mut().unwrap() } /// Retrieve the last node mutably pub fn last(&mut self) -> &mut VirtualNode { self.0.last_mut().unwrap() } } impl IntoIterator for IterableNodes { type Item = VirtualNode; // TODO: Is this possible with an array [VirtualNode] instead of a vec? type IntoIter = ::std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl From for IterableNodes { fn from(other: VirtualNode) -> Self { IterableNodes(vec![other]) } } impl From<&str> for IterableNodes { fn from(other: &str) -> Self { IterableNodes(vec![VirtualNode::text(other)]) } } impl From for IterableNodes { fn from(other: String) -> Self { IterableNodes(vec![VirtualNode::text(other.as_str())]) } } impl From> for IterableNodes { fn from(other: Vec) -> Self { IterableNodes(other) } } impl From> for IterableNodes { fn from(other: Vec) -> Self { IterableNodes(other.into_iter().map(|it| it.render()).collect()) } } impl From<&Vec> for IterableNodes { fn from(other: &Vec) -> Self { IterableNodes(other.iter().map(|it| it.render()).collect()) } } impl From<&[V]> for IterableNodes { fn from(other: &[V]) -> Self { IterableNodes(other.iter().map(|it| it.render()).collect()) } } impl From for VirtualNode { fn from(other: VText) -> Self { VirtualNode::Text(other) } } impl From for VirtualNode { fn from(other: VElement) -> Self { VirtualNode::Element(other) } } impl From<&str> for VirtualNode { fn from(other: &str) -> Self { VirtualNode::text(other) } } impl From for VirtualNode { fn from(other: String) -> Self { VirtualNode::text(other.as_str()) } } impl From<&str> for VText { fn from(text: &str) -> Self { VText { text: text.to_string(), } } } impl From for VText { fn from(text: String) -> Self { VText { text } } } impl IntoIterator for VirtualNode { type Item = VirtualNode; // TODO: Is this possible with an array [VirtualNode] instead of a vec? type IntoIter = ::std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { vec![self].into_iter() } } impl Into<::std::vec::IntoIter> for VirtualNode { fn into(self) -> ::std::vec::IntoIter { self.into_iter() } } impl fmt::Debug for VirtualNode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { VirtualNode::Element(e) => write!(f, "Node::{:?}", e), VirtualNode::Text(t) => write!(f, "Node::{:?}", t), VirtualNode::Component(c) => write!(f, "Node::{:?}", c), } } } impl fmt::Debug for VElement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "Element(<{}>, attrs: {:?}, children: {:?})", self.tag, self.attrs, self.children, ) } } impl fmt::Debug for VText { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Text({})", self.text) } } impl fmt::Display for VElement { // Turn a VElement and all of it's children (recursively) into an HTML string fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "<{}", self.tag).unwrap(); for (attr, value) in self.attrs.iter() { write!(f, r#" {}="{}""#, attr, value)?; } write!(f, ">")?; for child in self.children.iter() { write!(f, "{}", child.to_string())?; } if !html_validation::is_self_closing(&self.tag) { write!(f, "", self.tag)?; } Ok(()) } } // Turn a VText into an HTML string impl fmt::Display for VText { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.text) } } // Turn a VirtualNode into an HTML string (delegate impl to variants) impl fmt::Display for VirtualNode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { VirtualNode::Element(element) => write!(f, "{}", element), VirtualNode::Text(text) => write!(f, "{}", text), } } } /// Box>> is our js_sys::Closure. Stored this way to allow us to store /// any Closure regardless of the arguments. pub type DynClosure = Rc>; /// We need a custom implementation of fmt::Debug since JsValue doesn't /// implement debug. pub struct Events(pub HashMap); impl PartialEq for Events { // TODO: What should happen here..? And why? fn eq(&self, _rhs: &Self) -> bool { true } } impl fmt::Debug for Events { // Print out all of the event names for this VirtualNode fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let events: String = self.0.keys().map(|key| " ".to_string() + key).collect(); write!(f, "{}", events) } } #[cfg(test)] mod tests { use super::*; #[test] fn self_closing_tag_to_string() { let node = VirtualNode::element("br"); // No
since self closing tag assert_eq!(&node.to_string(), "
"); } #[test] fn to_string() { let mut node = VirtualNode::Element(VElement::new("div")); node.as_velement_mut() .unwrap() .attrs .insert("id".into(), "some-id".into()); let mut child = VirtualNode::Element(VElement::new("span")); let mut text = VirtualNode::Text(VText::new("Hello world")); child.as_velement_mut().unwrap().children.push(text); node.as_velement_mut().unwrap().children.push(child); let expected = r#"
Hello world
"#; assert_eq!(node.to_string(), expected); } }