windjammer_ui/
simple_vnode.rs

1//! Simple VNode implementation for generated component code
2//!
3//! This is a minimal virtual DOM implementation specifically for compiled components.
4//! It's designed to be simple and transparent, not a full-featured virtual DOM.
5
6use std::cell::RefCell;
7use std::rc::Rc;
8
9#[cfg(target_arch = "wasm32")]
10use wasm_bindgen::prelude::*;
11#[cfg(target_arch = "wasm32")]
12use web_sys::{Document, Element, Event};
13
14/// A simple virtual DOM node
15#[derive(Clone)]
16pub enum VNode {
17    Element {
18        tag: String,
19        attrs: Vec<(String, VAttr)>,
20        children: Vec<VNode>,
21    },
22    Text(String),
23}
24
25/// Attribute value
26#[derive(Clone)]
27pub enum VAttr {
28    Static(String),
29    Dynamic(String),
30    Event(Rc<RefCell<dyn FnMut()>>),
31}
32
33impl VNode {
34    /// Create an element node
35    pub fn element(tag: &str, attrs: Vec<(&str, VAttr)>, children: Vec<VNode>) -> Self {
36        VNode::Element {
37            tag: tag.to_string(),
38            attrs: attrs.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
39            children,
40        }
41    }
42
43    /// Create a text node
44    pub fn text(content: &str) -> Self {
45        VNode::Text(content.to_string())
46    }
47
48    /// Render this VNode to the DOM
49    #[cfg(target_arch = "wasm32")]
50    pub fn render(&self, document: &Document) -> Result<web_sys::Node, JsValue> {
51        match self {
52            VNode::Element {
53                tag,
54                attrs,
55                children,
56            } => {
57                let element = document.create_element(tag)?;
58
59                // Set attributes
60                for (name, value) in attrs {
61                    match value {
62                        VAttr::Static(v) | VAttr::Dynamic(v) => {
63                            if name == "class" {
64                                element.set_attribute("class", v)?;
65                            } else if name.starts_with("on_") {
66                                // Event handlers are handled separately
67                            } else {
68                                element.set_attribute(name, v)?;
69                            }
70                        }
71                        VAttr::Event(handler) => {
72                            if let Some(event_name) = name.strip_prefix("on_") {
73                                let handler_clone = handler.clone();
74                                let closure = Closure::wrap(Box::new(move |_event: Event| {
75                                    handler_clone.borrow_mut()();
76                                })
77                                    as Box<dyn FnMut(Event)>);
78
79                                element.add_event_listener_with_callback(
80                                    event_name,
81                                    closure.as_ref().unchecked_ref(),
82                                )?;
83                                closure.forget(); // Keep the closure alive
84                            }
85                        }
86                    }
87                }
88
89                // Render children
90                for child in children {
91                    let child_node = child.render(document)?;
92                    element.append_child(&child_node)?;
93                }
94
95                Ok(element.into())
96            }
97            VNode::Text(content) => {
98                let text_node = document.create_text_node(content);
99                Ok(text_node.into())
100            }
101        }
102    }
103
104    /// Mount this VNode to a parent element
105    #[cfg(target_arch = "wasm32")]
106    pub fn mount(&self, parent: &Element) -> Result<(), JsValue> {
107        let document = parent
108            .owner_document()
109            .ok_or_else(|| JsValue::from_str("No document"))?;
110        let node = self.render(&document)?;
111        parent.append_child(&node)?;
112        Ok(())
113    }
114}
115
116#[cfg(not(target_arch = "wasm32"))]
117impl VNode {
118    pub fn render(&self) -> Result<String, String> {
119        match self {
120            VNode::Element {
121                tag,
122                attrs: _,
123                children,
124            } => {
125                let mut html = format!("<{}>", tag);
126                for child in children {
127                    html.push_str(&child.render()?);
128                }
129                html.push_str(&format!("</{}>", tag));
130                Ok(html)
131            }
132            VNode::Text(content) => Ok(content.clone()),
133        }
134    }
135}