1use crate::{
7 ir::{NodeId, Value},
8 lifecycle::ModuleInstance,
9 reactive::{DependencyGraph, Scheduler},
10 reconcile::{evaluate_binding, reconcile_ir_node, InstanceTree, Patch},
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_full(scheduler, tree, module, &mut DependencyGraph::new(), None)
21}
22
23pub fn render_dirty_nodes_with_data_sources(
25 scheduler: &mut Scheduler,
26 tree: &mut InstanceTree,
27 module: Option<&ModuleInstance>,
28 data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
29) -> Vec<Patch> {
30 render_dirty_nodes_full(scheduler, tree, module, &mut DependencyGraph::new(), data_sources)
31}
32
33pub fn render_dirty_nodes_with_deps(
35 scheduler: &mut Scheduler,
36 tree: &mut InstanceTree,
37 module: Option<&ModuleInstance>,
38 dependencies: &mut DependencyGraph,
39) -> Vec<Patch> {
40 render_dirty_nodes_full(scheduler, tree, module, dependencies, None)
41}
42
43pub fn render_dirty_nodes_full(
45 scheduler: &mut Scheduler,
46 tree: &mut InstanceTree,
47 module: Option<&ModuleInstance>,
48 dependencies: &mut DependencyGraph,
49 data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
50) -> Vec<Patch> {
51 if !scheduler.has_dirty() {
52 return Vec::new();
53 }
54
55 let dirty_nodes = scheduler.take_dirty();
56 let state = module
57 .map(|m| m.get_state())
58 .unwrap_or(&serde_json::Value::Null);
59
60 let mut patches = Vec::new();
62 for node_id in dirty_nodes {
63 let is_list_node = tree
66 .get(node_id)
67 .map(|n| {
68 n.raw_props
69 .get("0")
70 .map(|v| matches!(v, Value::Binding(_)))
71 .unwrap_or(false)
72 && n.element_template.is_some()
73 })
74 .unwrap_or(false);
75
76 let is_control_flow = tree
78 .get(node_id)
79 .map(|n| n.ir_node_template.is_some())
80 .unwrap_or(false);
81
82 if is_list_node {
83 render_dirty_list(node_id, tree, state, &mut patches, dependencies, data_sources);
85 } else if is_control_flow {
86 let ir_template = tree
89 .get(node_id)
90 .and_then(|n| n.ir_node_template.clone());
91 if let Some(template) = ir_template {
92 reconcile_ir_node(
93 tree,
94 node_id,
95 &template,
96 state,
97 &mut patches,
98 dependencies,
99 data_sources,
100 );
101 }
102 } else {
103 let old_props = tree.get(node_id).map(|n| n.props.clone());
105
106 if let Some(node) = tree.get_mut(node_id) {
108 if data_sources.is_some() {
109 node.update_props_with_data_sources(state, data_sources);
110 } else {
111 node.update_props(state);
112 }
113 }
114
115 if let (Some(old), Some(node)) = (old_props, tree.get(node_id)) {
117 for (key, new_value) in &node.props {
118 if old.get(key) != Some(new_value) {
119 patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
120 }
121 }
122 for key in old.keys() {
124 if !node.props.contains_key(key) {
125 patches.push(Patch::remove_prop(node_id, key.clone()));
126 }
127 }
128 }
129 }
130 }
131
132 patches
133}
134
135fn render_dirty_list(
138 node_id: NodeId,
139 tree: &mut InstanceTree,
140 state: &serde_json::Value,
141 patches: &mut Vec<Patch>,
142 dependencies: &mut DependencyGraph,
143 data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
144) {
145 use crate::ir::Element;
146 use crate::reconcile::item_bindings::replace_item_bindings;
147 use crate::reconcile::keyed::reconcile_keyed_children;
148
149 let (array_binding, element_template, old_children) = {
151 let node = match tree.get(node_id) {
152 Some(n) => n,
153 None => return,
154 };
155
156 let binding = match node.raw_props.get("0") {
157 Some(Value::Binding(b)) => b.clone(),
158 _ => return,
159 };
160
161 let template = match &node.element_template {
162 Some(t) => t.clone(),
163 None => return, };
165
166 (binding, template, node.children.clone())
167 };
168
169 let array = evaluate_binding(&array_binding, state).unwrap_or(serde_json::Value::Array(vec![]));
171
172 let items = match &array {
173 serde_json::Value::Array(items) => items,
174 _ => return,
175 };
176
177 let mut new_children: Vec<Element> = Vec::new();
180
181 for (index, item) in items.iter().enumerate() {
182 for child_template in &element_template.children {
183 let child_with_item = replace_item_bindings(child_template, item, index);
188 new_children.push(child_with_item);
189 }
190 }
191
192 let old_children_vec: Vec<_> = old_children.iter().copied().collect();
195 let keyed_patches = reconcile_keyed_children(
196 tree,
197 node_id,
198 &old_children_vec,
199 &new_children,
200 state,
201 dependencies,
202 data_sources,
203 );
204
205 patches.extend(keyed_patches);
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::{ir::Element, lifecycle::Module, reactive::Binding};
212 use serde_json::json;
213
214 #[test]
215 fn test_render_dirty_nodes_no_dirty() {
216 let mut scheduler = Scheduler::new();
217 let mut tree = InstanceTree::new();
218
219 let patches = render_dirty_nodes(&mut scheduler, &mut tree, None);
220 assert_eq!(patches.len(), 0);
221 }
222
223 #[test]
224 fn test_render_dirty_nodes_with_changes() {
225 let mut scheduler = Scheduler::new();
226 let mut tree = InstanceTree::new();
227
228 let module = Module::new("TestModule");
230 let initial_state = json!({"count": 0});
231 let instance = ModuleInstance::new(module, initial_state);
232
233 let element = Element::new("Text");
235 let node_id = tree.create_node(&element, instance.get_state());
236
237 scheduler.mark_dirty(node_id);
239
240 let _patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
242
243 assert!(
245 !scheduler.has_dirty(),
246 "Scheduler should have no more dirty nodes"
247 );
248 }
249
250 #[test]
251 fn test_render_dirty_nodes_state_change() {
252 use crate::ir::Value;
253
254 let mut scheduler = Scheduler::new();
255 let mut tree = InstanceTree::new();
256
257 let module = Module::new("TestModule");
259 let initial_state = json!({"text": "Hello"});
260 let mut instance = ModuleInstance::new(module, initial_state);
261
262 let mut element = Element::new("Text");
264 element.props.insert(
265 "0".to_string(),
266 Value::Binding(Binding::state(vec!["text".to_string()])),
267 );
268 let node_id = tree.create_node(&element, instance.get_state());
269
270 if let Some(node) = tree.get_mut(node_id) {
272 node.update_props(instance.get_state());
273 }
274
275 instance.update_state(json!({"text": "World"}));
277
278 scheduler.mark_dirty(node_id);
280
281 let patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
283
284 let set_prop_count = patches
286 .iter()
287 .filter(|p| matches!(p, Patch::SetProp { .. }))
288 .count();
289 assert!(
290 set_prop_count > 0,
291 "Should have SetProp patches for changed state"
292 );
293 }
294
295 #[test]
296 fn test_render_dirty_nodes_multiple_nodes() {
297 let mut scheduler = Scheduler::new();
298 let mut tree = InstanceTree::new();
299
300 let module = Module::new("TestModule");
301 let initial_state = json!({});
302 let instance = ModuleInstance::new(module, initial_state);
303
304 let element1 = Element::new("Text");
306 let element2 = Element::new("Text");
307 let element3 = Element::new("Text");
308
309 let node_id_1 = tree.create_node(&element1, instance.get_state());
310 let _node_id_2 = tree.create_node(&element2, instance.get_state());
311 let node_id_3 = tree.create_node(&element3, instance.get_state());
312
313 scheduler.mark_dirty(node_id_1);
315 scheduler.mark_dirty(node_id_3);
316
317 assert!(scheduler.has_dirty());
319
320 render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
321
322 assert!(!scheduler.has_dirty());
324 }
325}