123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- //! 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<u32> = Mutex::new(0);
- }
- /// When building your views you'll typically use the `html!` macro to generate
- /// `VirtualNode`'s.
- ///
- /// `html! { <div> <span></span> </div> }` 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<String, String>,
- /// Events that will get added to your real DOM element via `.addEventListener`
- pub events: Events,
- /// The children of this `VirtualNode`. So a <div> <em></em> </div> structure would
- /// have a parent div and one child, em.
- pub children: Vec<VirtualNode>,
- }
- #[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<S>(tag: S) -> Self
- where
- S: Into<String>,
- {
- 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<S>(text: S) -> Self
- where
- S: Into<String>,
- {
- 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<Node> {
- 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! { <div> {world}</div> }
- ///
- /// So that we end up with <div> world</div> 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! { <div>{Hello} {world}</div> } -> <div>Hello world</div>
- /// html! { <div>{Hello} </div> } -> <div>Hello </div>
- ///
- /// So that we end up with <div>Hello world</div> 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<S>(tag: S) -> Self
- where
- S: Into<String>,
- {
- 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<Element> {
- 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<S>(text: S) -> Self
- where
- S: Into<String>,
- {
- 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<T> {
- /// 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<u32, Vec<DynClosure>>,
- }
- impl<T> CreatedNode<T> {
- pub fn without_closures<N: Into<T>>(node: N) -> Self {
- CreatedNode {
- node: node.into(),
- closures: HashMap::with_capacity(0),
- }
- }
- }
- impl<T> Deref for CreatedNode<T> {
- type Target = T;
- fn deref(&self) -> &Self::Target {
- &self.node
- }
- }
- impl From<CreatedNode<Element>> for CreatedNode<Node> {
- fn from(other: CreatedNode<Element>) -> CreatedNode<Node> {
- 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<VirtualNode>
- fn render(&self) -> VirtualNode;
- }
- impl<V> 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<IterableNodes>
- ///
- /// html! { <div> { nodes } </div> }
- ///
- /// nodes can be a String .. VirtualNode .. Vec<VirtualNode> ... etc
- pub struct IterableNodes(Vec<VirtualNode>);
- 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<VirtualNode>;
- fn into_iter(self) -> Self::IntoIter {
- self.0.into_iter()
- }
- }
- impl From<VirtualNode> 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<String> for IterableNodes {
- fn from(other: String) -> Self {
- IterableNodes(vec![VirtualNode::text(other.as_str())])
- }
- }
- impl From<Vec<VirtualNode>> for IterableNodes {
- fn from(other: Vec<VirtualNode>) -> Self {
- IterableNodes(other)
- }
- }
- impl<V: View> From<Vec<V>> for IterableNodes {
- fn from(other: Vec<V>) -> Self {
- IterableNodes(other.into_iter().map(|it| it.render()).collect())
- }
- }
- impl<V: View> From<&Vec<V>> for IterableNodes {
- fn from(other: &Vec<V>) -> Self {
- IterableNodes(other.iter().map(|it| it.render()).collect())
- }
- }
- impl<V: View> From<&[V]> for IterableNodes {
- fn from(other: &[V]) -> Self {
- IterableNodes(other.iter().map(|it| it.render()).collect())
- }
- }
- impl From<VText> for VirtualNode {
- fn from(other: VText) -> Self {
- VirtualNode::Text(other)
- }
- }
- impl From<VElement> 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<String> 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<String> 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<VirtualNode>;
- fn into_iter(self) -> Self::IntoIter {
- vec![self].into_iter()
- }
- }
- impl Into<::std::vec::IntoIter<VirtualNode>> for VirtualNode {
- fn into(self) -> ::std::vec::IntoIter<VirtualNode> {
- 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<dyn AsRef<JsValue>>> is our js_sys::Closure. Stored this way to allow us to store
- /// any Closure regardless of the arguments.
- pub type DynClosure = Rc<dyn AsRef<JsValue>>;
- /// We need a custom implementation of fmt::Debug since JsValue doesn't
- /// implement debug.
- pub struct Events(pub HashMap<String, DynClosure>);
- 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 </br> since self closing tag
- assert_eq!(&node.to_string(), "<br>");
- }
- #[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#"<div id="some-id"><span>Hello world</span></div>"#;
- assert_eq!(node.to_string(), expected);
- }
- }
|