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}