use crate::{
ir::{NodeId, Value},
lifecycle::ModuleInstance,
reactive::{DependencyGraph, Scheduler},
reconcile::{evaluate_binding, reconcile_ir_node, InstanceTree, Patch},
};
pub fn render_dirty_nodes(
scheduler: &mut Scheduler,
tree: &mut InstanceTree,
module: Option<&ModuleInstance>,
) -> Vec<Patch> {
render_dirty_nodes_full(scheduler, tree, module, &mut DependencyGraph::new(), None)
}
pub fn render_dirty_nodes_with_data_sources(
scheduler: &mut Scheduler,
tree: &mut InstanceTree,
module: Option<&ModuleInstance>,
data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
) -> Vec<Patch> {
render_dirty_nodes_full(scheduler, tree, module, &mut DependencyGraph::new(), data_sources)
}
pub fn render_dirty_nodes_with_deps(
scheduler: &mut Scheduler,
tree: &mut InstanceTree,
module: Option<&ModuleInstance>,
dependencies: &mut DependencyGraph,
) -> Vec<Patch> {
render_dirty_nodes_full(scheduler, tree, module, dependencies, None)
}
pub fn render_dirty_nodes_full(
scheduler: &mut Scheduler,
tree: &mut InstanceTree,
module: Option<&ModuleInstance>,
dependencies: &mut DependencyGraph,
data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
) -> Vec<Patch> {
if !scheduler.has_dirty() {
return Vec::new();
}
let dirty_nodes = scheduler.take_dirty();
let state = module
.map(|m| m.get_state())
.unwrap_or(&serde_json::Value::Null);
let mut patches = Vec::new();
for node_id in dirty_nodes {
let is_list_node = tree
.get(node_id)
.map(|n| {
n.raw_props
.get("0")
.map(|v| matches!(v, Value::Binding(_)))
.unwrap_or(false)
&& n.element_template.is_some()
})
.unwrap_or(false);
let is_control_flow = tree
.get(node_id)
.map(|n| n.ir_node_template.is_some())
.unwrap_or(false);
if is_list_node {
render_dirty_list(node_id, tree, state, &mut patches, dependencies, data_sources);
} else if is_control_flow {
let ir_template = tree
.get(node_id)
.and_then(|n| n.ir_node_template.clone());
if let Some(template) = ir_template {
reconcile_ir_node(
tree,
node_id,
&template,
state,
&mut patches,
dependencies,
data_sources,
);
}
} else {
let old_props = tree.get(node_id).map(|n| n.props.clone());
if let Some(node) = tree.get_mut(node_id) {
if data_sources.is_some() {
node.update_props_with_data_sources(state, data_sources);
} else {
node.update_props(state);
}
}
if let (Some(old), Some(node)) = (old_props, tree.get(node_id)) {
for (key, new_value) in &node.props {
if old.get(key) != Some(new_value) {
patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
}
}
for key in old.keys() {
if !node.props.contains_key(key) {
patches.push(Patch::remove_prop(node_id, key.clone()));
}
}
}
}
}
patches
}
fn render_dirty_list(
node_id: NodeId,
tree: &mut InstanceTree,
state: &serde_json::Value,
patches: &mut Vec<Patch>,
dependencies: &mut DependencyGraph,
data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
) {
use crate::ir::Element;
use crate::reconcile::item_bindings::replace_item_bindings;
use crate::reconcile::keyed::reconcile_keyed_children;
let (array_binding, element_template, old_children) = {
let node = match tree.get(node_id) {
Some(n) => n,
None => return,
};
let binding = match node.raw_props.get("0") {
Some(Value::Binding(b)) => b.clone(),
_ => return,
};
let template = match &node.element_template {
Some(t) => t.clone(),
None => return, };
(binding, template, node.children.clone())
};
let array = evaluate_binding(&array_binding, state).unwrap_or(serde_json::Value::Array(vec![]));
let items = match &array {
serde_json::Value::Array(items) => items,
_ => return,
};
let mut new_children: Vec<Element> = Vec::new();
for (index, item) in items.iter().enumerate() {
for child_template in &element_template.children {
let child_with_item = replace_item_bindings(child_template, item, index);
new_children.push(child_with_item);
}
}
let old_children_vec: Vec<_> = old_children.iter().copied().collect();
let keyed_patches = reconcile_keyed_children(
tree,
node_id,
&old_children_vec,
&new_children,
state,
dependencies,
data_sources,
);
patches.extend(keyed_patches);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ir::Element, lifecycle::Module, reactive::Binding};
use serde_json::json;
#[test]
fn test_render_dirty_nodes_no_dirty() {
let mut scheduler = Scheduler::new();
let mut tree = InstanceTree::new();
let patches = render_dirty_nodes(&mut scheduler, &mut tree, None);
assert_eq!(patches.len(), 0);
}
#[test]
fn test_render_dirty_nodes_with_changes() {
let mut scheduler = Scheduler::new();
let mut tree = InstanceTree::new();
let module = Module::new("TestModule");
let initial_state = json!({"count": 0});
let instance = ModuleInstance::new(module, initial_state);
let element = Element::new("Text");
let node_id = tree.create_node(&element, instance.get_state());
scheduler.mark_dirty(node_id);
let _patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
assert!(
!scheduler.has_dirty(),
"Scheduler should have no more dirty nodes"
);
}
#[test]
fn test_render_dirty_nodes_state_change() {
use crate::ir::Value;
let mut scheduler = Scheduler::new();
let mut tree = InstanceTree::new();
let module = Module::new("TestModule");
let initial_state = json!({"text": "Hello"});
let mut instance = ModuleInstance::new(module, initial_state);
let mut element = Element::new("Text");
element.props.insert(
"0".to_string(),
Value::Binding(Binding::state(vec!["text".to_string()])),
);
let node_id = tree.create_node(&element, instance.get_state());
if let Some(node) = tree.get_mut(node_id) {
node.update_props(instance.get_state());
}
instance.update_state(json!({"text": "World"}));
scheduler.mark_dirty(node_id);
let patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
let set_prop_count = patches
.iter()
.filter(|p| matches!(p, Patch::SetProp { .. }))
.count();
assert!(
set_prop_count > 0,
"Should have SetProp patches for changed state"
);
}
#[test]
fn test_render_dirty_nodes_multiple_nodes() {
let mut scheduler = Scheduler::new();
let mut tree = InstanceTree::new();
let module = Module::new("TestModule");
let initial_state = json!({});
let instance = ModuleInstance::new(module, initial_state);
let element1 = Element::new("Text");
let element2 = Element::new("Text");
let element3 = Element::new("Text");
let node_id_1 = tree.create_node(&element1, instance.get_state());
let _node_id_2 = tree.create_node(&element2, instance.get_state());
let node_id_3 = tree.create_node(&element3, instance.get_state());
scheduler.mark_dirty(node_id_1);
scheduler.mark_dirty(node_id_3);
assert!(scheduler.has_dirty());
render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
assert!(!scheduler.has_dirty());
}
}