Skip to main content

rue_core/
app.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3use wasm_bindgen::JsValue;
4
5use crate::component::Component;
6use crate::node::{mount::mount_to_dom, patch::patch_node, VNode};
7
8/// The Rue application.
9pub struct App {
10    /// The root VNode render function.
11    root_fn: Box<dyn Fn() -> VNode>,
12    /// The mount point selector (CSS selector).
13    selector: String,
14    /// The mount point DOM element.
15    mount_element: Option<web_sys::Element>,
16    /// The previously rendered VNode tree (used for diffing).
17    old_vnode: Option<VNode>,
18    /// Shared component reference for lifecycle hooks.
19    component_ctrl: Option<Rc<RefCell<Box<dyn Component>>>>,
20}
21
22impl App {
23    /// Create a new application from a render closure.
24    ///
25    /// This is the low-level constructor. Prefer [`App::from_component`] for
26    /// lifecycle-managed components.
27    pub fn new<F: Fn() -> VNode + 'static>(selector: &str, root_fn: F) -> Self {
28        App {
29            root_fn: Box::new(root_fn),
30            selector: selector.to_string(),
31            mount_element: None,
32            old_vnode: None,
33            component_ctrl: None,
34        }
35    }
36
37    /// Create a new application from a [`Component`].
38    ///
39    /// This will call [`Component::init()`] once, then use [`Component::render()`]
40    /// for every render cycle. After the first mount, [`Component::mounted()`]
41    /// is called. Before each update, [`Component::should_update()`] is checked.
42    ///
43    /// # Example
44    ///
45    /// ```ignore
46    /// let app = App::from_component("#app", MyComponent::new());
47    /// app.mount()?;
48    /// ```
49    pub fn from_component<T: Component>(selector: &str, mut component: T) -> Self {
50        // 1. Call init() lifecycle
51        component.init();
52
53        // 2. Box and share for closure + lifecycle access
54        let comp: Rc<RefCell<Box<dyn Component>>> =
55            Rc::new(RefCell::new(Box::new(component)));
56
57        // 3. Create render closure that calls component.render()
58        let comp_clone = comp.clone();
59        let root_fn = Box::new(move || -> VNode {
60            comp_clone.borrow().render()
61        });
62
63        App {
64            root_fn,
65            selector: selector.to_string(),
66            mount_element: None,
67            old_vnode: None,
68            component_ctrl: Some(comp),
69        }
70    }
71
72    /// Mount the application to the DOM.
73    ///
74    /// Renders the initial VNode tree, inserts it into the mount point,
75    /// and calls [`Component::mounted()`] if a component was provided.
76    pub fn mount(&mut self) -> Result<(), JsValue> {
77        let window = web_sys::window().ok_or("no window")?;
78        let document = window.document().ok_or("no document")?;
79        let mount_point = document
80            .query_selector(&self.selector)
81            .ok()
82            .flatten()
83            .ok_or("mount point not found")?;
84
85        // Clear the mount point
86        mount_point.set_inner_html("");
87
88        // Render the root VNode
89        let vnode = (self.root_fn)();
90        let parent: web_sys::Node = mount_point.clone().into();
91        let _ = mount_to_dom(&vnode, &parent, None);
92
93        // Store state for future updates
94        self.mount_element = Some(mount_point);
95        self.old_vnode = Some(vnode);
96
97        // Call mounted() lifecycle
98        if let Some(ref comp) = self.component_ctrl {
99            let borrowed = comp.borrow();
100            borrowed.mounted();
101        }
102
103        Ok(())
104    }
105
106    /// Update the application by diffing the old and new VNode trees and
107    /// applying only the necessary changes to the DOM.
108    ///
109    /// Before re-rendering, calls [`Component::should_update()`] if a component
110    /// was provided. Skips the update if it returns `false`.
111    ///
112    /// Unlike the previous naive implementation (which destroyed and recreated
113    /// all DOM nodes), this preserves scroll position, input focus, and
114    /// form state for unchanged elements.
115    pub fn update(&mut self) -> Result<(), JsValue> {
116        // Check should_update() lifecycle
117        if let Some(ref comp) = self.component_ctrl {
118            let borrowed = comp.borrow();
119            if !borrowed.should_update() {
120                return Ok(());
121            }
122        }
123
124        let mount_point = self
125            .mount_element
126            .as_ref()
127            .ok_or_else(|| JsValue::from_str("App not mounted"))?;
128
129        // Render new VNode tree
130        let new_vnode = (self.root_fn)();
131
132        // If we have a previous tree, patch in-place
133        if let Some(ref old_vnode) = self.old_vnode {
134            let parent: web_sys::Node = mount_point.clone().into();
135            patch_node(old_vnode, &new_vnode, &parent, None);
136        } else {
137            // No previous tree — mount fresh
138            let parent: web_sys::Node = mount_point.clone().into();
139            mount_to_dom(&new_vnode, &parent, None);
140        }
141
142        // Store new tree for next diff
143        self.old_vnode = Some(new_vnode);
144
145        Ok(())
146    }
147
148    /// Get a reference to the mount element, if mounted.
149    pub fn mount_element(&self) -> Option<&web_sys::Element> {
150        self.mount_element.as_ref()
151    }
152}
153
154/// Mount an application to the DOM (convenience function).
155///
156/// Creates an `App` from a render closure, mounts it, and returns the `App`.
157pub fn mount<F: Fn() -> VNode + 'static>(selector: &str, root_fn: F) -> App {
158    let mut app = App::new(selector, root_fn);
159    let _ = app.mount();
160    app
161}