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