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