Skip to main content

virtual_node/
lib.rs

1//! The virtual_node module exposes the `VirtualNode` struct and methods that power our
2//! virtual dom.
3
4// TODO: A few of these dependencies (including js_sys) are used to power events.. yet events
5// only work on wasm32 targets. So we should start sprinkling some
6//
7// #[cfg(target_arch = "wasm32")]
8// #[cfg(not(target_arch = "wasm32"))]
9//
10// Around in order to get rid of dependencies that we don't need in non wasm32 targets
11
12use std::fmt;
13
14#[cfg(feature = "web")]
15pub use self::create_element::VIRTUAL_NODE_MARKER_PROPERTY;
16#[cfg(feature = "web")]
17pub use self::event::EventAttribFn;
18pub use self::iterable_nodes::*;
19pub use self::velement::*;
20pub use self::vtext::*;
21
22#[cfg(feature = "web")]
23pub mod event;
24pub mod test_utils;
25
26#[cfg(feature = "web")]
27mod create_element;
28
29mod iterable_nodes;
30mod velement;
31mod vtext;
32
33/// When building your views you'll typically use the `html!` macro to generate
34/// `VirtualNode`'s.
35///
36/// `html! { <div> <span></span> </div> }` really generates a `VirtualNode` with
37/// one child (span).
38///
39/// Later, on the client side, you'll use the `diff` and `patch` modules to
40/// update the real DOM with your latest tree of virtual nodes (virtual dom).
41///
42/// Or on the server side you'll just call `.to_string()` on your root virtual node
43/// in order to recursively render the node and all of its children.
44///
45/// TODO: Make all of these fields private and create accessor methods
46/// TODO: Create a builder to create instances of VirtualNode::Element with
47/// attrs and children without having to explicitly create a VElement
48#[derive(PartialEq)]
49pub enum VirtualNode {
50    /// An element node (node type `ELEMENT_NODE`).
51    Element(VElement),
52    /// A text node (node type `TEXT_NODE`).
53    ///
54    /// Note: This wraps a `VText` instead of a plain `String` in
55    /// order to enable custom methods like `create_text_node()` on the
56    /// wrapped type.
57    Text(VText),
58}
59
60impl VirtualNode {
61    /// Create a new virtual element node with a given tag.
62    ///
63    /// These get patched into the DOM using `document.createElement`
64    ///
65    /// ```
66    /// # use virtual_node::VirtualNode;
67    /// let _div = VirtualNode::element("div");
68    /// ```
69    // FIXME: Rename to new_element
70    pub fn element<S>(tag: S) -> Self
71    where
72        S: Into<String>,
73    {
74        VirtualNode::Element(VElement::new(tag))
75    }
76
77    /// Create a new virtual text node with the given text.
78    ///
79    /// These get patched into the DOM using `document.createTextNode`
80    ///
81    /// ```
82    /// # use virtual_node::VirtualNode;
83    /// let _text = VirtualNode::text("My text node");
84    /// ```
85    // FIXME: Rename to new_text
86    pub fn text<S>(text: S) -> Self
87    where
88        S: Into<String>,
89    {
90        VirtualNode::Text(VText::new(text.into()))
91    }
92
93    /// Return a [`VElement`] reference, if this is an [`Element`] variant.
94    ///
95    /// [`VElement`]: struct.VElement.html
96    /// [`Element`]: enum.VirtualNode.html#variant.Element
97    // TODO: Rename to .as_velement()
98    pub fn as_velement_ref(&self) -> Option<&VElement> {
99        match self {
100            VirtualNode::Element(ref element_node) => Some(element_node),
101            _ => None,
102        }
103    }
104
105    /// Return a mutable [`VElement`] reference, if this is an [`Element`] variant.
106    ///
107    /// [`VElement`]: struct.VElement.html
108    /// [`Element`]: enum.VirtualNode.html#variant.Element
109    pub fn as_velement_mut(&mut self) -> Option<&mut VElement> {
110        match self {
111            VirtualNode::Element(ref mut element_node) => Some(element_node),
112            _ => None,
113        }
114    }
115
116    /// Return a [`VText`] reference, if this is an [`Text`] variant.
117    ///
118    /// [`VText`]: struct.VText.html
119    /// [`Text`]: enum.VirtualNode.html#variant.Text
120    // TODO: Rename to .as_vtext()
121    pub fn as_vtext_ref(&self) -> Option<&VText> {
122        match self {
123            VirtualNode::Text(ref text_node) => Some(text_node),
124            _ => None,
125        }
126    }
127
128    /// Return a mutable [`VText`] reference, if this is an [`Text`] variant.
129    ///
130    /// [`VText`]: struct.VText.html
131    /// [`Text`]: enum.VirtualNode.html#variant.Text
132    pub fn as_vtext_mut(&mut self) -> Option<&mut VText> {
133        match self {
134            VirtualNode::Text(ref mut text_node) => Some(text_node),
135            _ => None,
136        }
137    }
138
139    /// Create and return a [`web_sys::Node`] along with its events.
140    #[cfg(feature = "web")]
141    pub fn create_dom_node(
142        &self,
143        events: &mut self::event::VirtualEvents,
144    ) -> (web_sys::Node, crate::event::VirtualEventNode) {
145        match self {
146            VirtualNode::Text(text_node) => (
147                text_node.create_text_node().into(),
148                events.create_text_node(),
149            ),
150            VirtualNode::Element(element_node) => {
151                let (elem, events) = element_node.create_element_node(events);
152                (elem.into(), events)
153            }
154        }
155    }
156
157    /// Used by html-macro to insert space before text that is inside of a block that came after
158    /// an open tag.
159    ///
160    /// html! { <div> {world}</div> }
161    ///
162    /// So that we end up with <div> world</div> when we're finished parsing.
163    pub fn insert_space_before_text(&mut self) {
164        match self {
165            VirtualNode::Text(text_node) => {
166                text_node.text = " ".to_string() + &text_node.text;
167            }
168            _ => {}
169        }
170    }
171
172    /// Used by html-macro to insert space after braced text if we know that the next block is
173    /// another block or a closing tag.
174    ///
175    /// html! { <div>{Hello} {world}</div> } -> <div>Hello world</div>
176    /// html! { <div>{Hello} </div> } -> <div>Hello </div>
177    ///
178    /// So that we end up with <div>Hello world</div> when we're finished parsing.
179    pub fn insert_space_after_text(&mut self) {
180        match self {
181            VirtualNode::Text(text_node) => {
182                text_node.text += " ";
183            }
184            _ => {}
185        }
186    }
187}
188
189/// A trait with common functionality for rendering front-end views.
190pub trait View {
191    /// Render a VirtualNode, or any IntoIter<VirtualNode>
192    fn render(&self) -> VirtualNode;
193}
194
195impl<V> From<&V> for VirtualNode
196where
197    V: View,
198{
199    fn from(v: &V) -> Self {
200        v.render()
201    }
202}
203
204impl From<VText> for VirtualNode {
205    fn from(other: VText) -> Self {
206        VirtualNode::Text(other)
207    }
208}
209
210impl From<VElement> for VirtualNode {
211    fn from(other: VElement) -> Self {
212        VirtualNode::Element(other)
213    }
214}
215
216impl From<&str> for VirtualNode {
217    fn from(other: &str) -> Self {
218        VirtualNode::text(other)
219    }
220}
221
222impl From<String> for VirtualNode {
223    fn from(other: String) -> Self {
224        VirtualNode::text(other.as_str())
225    }
226}
227
228impl IntoIterator for VirtualNode {
229    type Item = VirtualNode;
230    // TODO: ::std::iter::Once<VirtualNode> to avoid allocation
231    type IntoIter = ::std::vec::IntoIter<VirtualNode>;
232
233    fn into_iter(self) -> Self::IntoIter {
234        vec![self].into_iter()
235    }
236}
237
238impl Into<::std::vec::IntoIter<VirtualNode>> for VirtualNode {
239    fn into(self) -> ::std::vec::IntoIter<VirtualNode> {
240        self.into_iter()
241    }
242}
243
244impl fmt::Debug for VirtualNode {
245    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
246        match self {
247            VirtualNode::Element(e) => write!(f, "Node::{:?}", e),
248            VirtualNode::Text(t) => write!(f, "Node::{:?}", t),
249        }
250    }
251}
252
253// Turn a VirtualNode into an HTML string (delegate impl to variants)
254impl fmt::Display for VirtualNode {
255    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
256        match self {
257            VirtualNode::Element(element) => write!(f, "{}", element),
258            VirtualNode::Text(text) => write!(f, "{}", text),
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn self_closing_tag_to_string() {
269        let node = VirtualNode::element("br");
270
271        // No </br> since self closing tag
272        assert_eq!(&node.to_string(), "<br>");
273    }
274
275    #[test]
276    fn to_string() {
277        let mut node = VirtualNode::Element(VElement::new("div"));
278        node.as_velement_mut()
279            .unwrap()
280            .attrs
281            .insert("id".into(), "some-id".into());
282
283        let mut child = VirtualNode::Element(VElement::new("span"));
284
285        let text = VirtualNode::Text(VText::new("Hello world"));
286
287        child.as_velement_mut().unwrap().children.push(text);
288
289        node.as_velement_mut().unwrap().children.push(child);
290
291        let expected = r#"<div id="some-id"><span>Hello world</span></div>"#;
292
293        assert_eq!(node.to_string(), expected);
294    }
295
296    /// Verify that a boolean attribute is included in the string if true.
297    #[test]
298    fn boolean_attribute_true_shown() {
299        let mut button = VElement::new("button");
300        button.attrs.insert("disabled".into(), true.into());
301
302        let expected = "<button disabled></button>";
303        let button = VirtualNode::Element(button).to_string();
304
305        assert_eq!(button.to_string(), expected);
306    }
307
308    /// Verify that a boolean attribute is not included in the string if false.
309    #[test]
310    fn boolean_attribute_false_ignored() {
311        let mut button = VElement::new("button");
312        button.attrs.insert("disabled".into(), false.into());
313
314        let expected = "<button></button>";
315        let button = VirtualNode::Element(button).to_string();
316
317        assert_eq!(button.to_string(), expected);
318    }
319}