1use crate::{
7 ir::{NodeId, Value},
8 lifecycle::ModuleInstance,
9 reconcile::{InstanceTree, Patch},
10 reactive::{DependencyGraph, Scheduler},
11};
12
13pub fn render_dirty_nodes(
16 scheduler: &mut Scheduler,
17 tree: &mut InstanceTree,
18 module: Option<&ModuleInstance>,
19) -> Vec<Patch> {
20 render_dirty_nodes_with_deps(scheduler, tree, module, &mut DependencyGraph::new())
21}
22
23pub fn render_dirty_nodes_with_deps(
25 scheduler: &mut Scheduler,
26 tree: &mut InstanceTree,
27 module: Option<&ModuleInstance>,
28 dependencies: &mut DependencyGraph,
29) -> Vec<Patch> {
30 if !scheduler.has_dirty() {
31 return Vec::new();
32 }
33
34 let dirty_nodes = scheduler.take_dirty();
35 let state = module
36 .map(|m| m.get_state())
37 .unwrap_or(&serde_json::Value::Null);
38
39 let mut patches = Vec::new();
41 for node_id in dirty_nodes {
42 let is_list_node = tree.get(node_id)
45 .map(|n| {
46 n.raw_props.get("0").map(|v| matches!(v, Value::Binding(_))).unwrap_or(false)
47 && n.element_template.is_some()
48 })
49 .unwrap_or(false);
50
51 if is_list_node {
52 render_dirty_list(node_id, tree, state, &mut patches, dependencies);
54 } else {
55 let old_props = tree.get(node_id).map(|n| n.props.clone());
57
58 if let Some(node) = tree.get_mut(node_id) {
60 node.update_props(state);
61 }
62
63 if let (Some(old), Some(node)) = (old_props, tree.get(node_id)) {
65 for (key, new_value) in &node.props {
66 if old.get(key) != Some(new_value) {
68 patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
69 }
70 }
71 }
72 }
73 }
74
75 patches
76}
77
78fn render_dirty_list(
80 node_id: NodeId,
81 tree: &mut InstanceTree,
82 state: &serde_json::Value,
83 patches: &mut Vec<Patch>,
84 dependencies: &mut DependencyGraph,
85) {
86 use crate::reconcile::diff::{create_tree, replace_item_bindings};
87
88 let (array_binding, element_template, old_children) = {
90 let node = match tree.get(node_id) {
91 Some(n) => n,
92 None => return,
93 };
94
95 let binding = match node.raw_props.get("0") {
96 Some(Value::Binding(b)) => b.clone(),
97 _ => return,
98 };
99
100 let template = match &node.element_template {
101 Some(t) => t.clone(),
102 None => return, };
104
105 (binding, template, node.children.clone())
106 };
107
108 let array = evaluate_binding(&array_binding, state)
110 .unwrap_or(serde_json::Value::Array(vec![]));
111
112 let items = match &array {
113 serde_json::Value::Array(items) => items,
114 _ => return,
115 };
116
117 for &child_id in &old_children {
119 patches.push(Patch::remove(child_id));
120 tree.remove(child_id);
121 }
122
123 if let Some(node) = tree.get_mut(node_id) {
125 node.children.clear();
126 }
127
128 for (index, item) in items.iter().enumerate() {
130 for child_template in &element_template.children {
131 let child_with_item = replace_item_bindings(child_template, item, index);
133 create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
134 }
135 }
136}
137
138fn evaluate_binding(binding: &crate::reactive::Binding, state: &serde_json::Value) -> Option<serde_json::Value> {
140 let mut current = state;
141 for segment in &binding.path {
142 current = current.get(segment)?;
143 }
144 Some(current.clone())
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::{ir::Element, lifecycle::Module, reactive::Binding};
151 use serde_json::json;
152
153 #[test]
154 fn test_render_dirty_nodes_no_dirty() {
155 let mut scheduler = Scheduler::new();
156 let mut tree = InstanceTree::new();
157
158 let patches = render_dirty_nodes(&mut scheduler, &mut tree, None);
159 assert_eq!(patches.len(), 0);
160 }
161
162 #[test]
163 fn test_render_dirty_nodes_with_changes() {
164 let mut scheduler = Scheduler::new();
165 let mut tree = InstanceTree::new();
166
167 let module = Module::new("TestModule");
169 let initial_state = json!({"count": 0});
170 let instance = ModuleInstance::new(module, initial_state);
171
172 let element = Element::new("Text");
174 let node_id = tree.create_node(&element, instance.get_state());
175
176 scheduler.mark_dirty(node_id);
178
179 let patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
181
182 assert!(!scheduler.has_dirty(), "Scheduler should have no more dirty nodes");
184 }
185
186 #[test]
187 fn test_render_dirty_nodes_state_change() {
188 use crate::ir::Value;
189
190 let mut scheduler = Scheduler::new();
191 let mut tree = InstanceTree::new();
192
193 let module = Module::new("TestModule");
195 let initial_state = json!({"text": "Hello"});
196 let mut instance = ModuleInstance::new(module, initial_state);
197
198 let mut element = Element::new("Text");
200 element
201 .props
202 .insert("0".to_string(), Value::Binding(Binding::new(vec!["text".to_string()])));
203 let node_id = tree.create_node(&element, instance.get_state());
204
205 if let Some(node) = tree.get_mut(node_id) {
207 node.update_props(instance.get_state());
208 }
209
210 instance.update_state(json!({"text": "World"}));
212
213 scheduler.mark_dirty(node_id);
215
216 let patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
218
219 let set_prop_count = patches
221 .iter()
222 .filter(|p| matches!(p, Patch::SetProp { .. }))
223 .count();
224 assert!(set_prop_count > 0, "Should have SetProp patches for changed state");
225 }
226
227 #[test]
228 fn test_render_dirty_nodes_multiple_nodes() {
229 let mut scheduler = Scheduler::new();
230 let mut tree = InstanceTree::new();
231
232 let module = Module::new("TestModule");
233 let initial_state = json!({});
234 let instance = ModuleInstance::new(module, initial_state);
235
236 let element1 = Element::new("Text");
238 let element2 = Element::new("Text");
239 let element3 = Element::new("Text");
240
241 let node_id_1 = tree.create_node(&element1, instance.get_state());
242 let node_id_2 = tree.create_node(&element2, instance.get_state());
243 let node_id_3 = tree.create_node(&element3, instance.get_state());
244
245 scheduler.mark_dirty(node_id_1);
247 scheduler.mark_dirty(node_id_3);
248
249 assert!(scheduler.has_dirty());
251
252 render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
253
254 assert!(!scheduler.has_dirty());
256 }
257}