use crate::{
ir::{NodeId, Value},
lifecycle::ModuleInstance,
reactive::{DependencyGraph, Scheduler},
reconcile::{evaluate_binding, reconcile_ir_node_impl, InstanceTree, Patch, ReconcileCtx},
};
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,
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,
None,
)
}
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, 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>>,
modules: Option<&indexmap::IndexMap<String, ModuleInstance>>,
) -> 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 {
let mut ctx = ReconcileCtx {
tree,
state,
patches: &mut patches,
dependencies,
data_sources,
modules,
};
reconcile_ir_node_impl(&mut ctx, node_id, &template);
}
} else {
let old_props = tree.get(node_id).map(|n| n.props.clone());
let effective_state = tree
.get(node_id)
.and_then(|n| n.module_scope.as_deref())
.and_then(|scope| modules.and_then(|m| m.get(scope)))
.map(|m| m.get_state())
.unwrap_or(state);
if let Some(node) = tree.get_mut(node_id) {
if data_sources.is_some() {
node.update_props_with_data_sources(effective_state, data_sources);
} else {
node.update_props(effective_state);
}
}
if let (Some(old), Some(node)) = (old_props, tree.get(node_id)) {
for (key, new_value) in node.props.iter() {
if old.get(key.as_str()) != 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.as_str()) {
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::reconcile::keyed::reconcile_iterable_children;
let (array_binding, element_template, key_path_owned) = {
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,
};
let key_path = template.props.get("key.0").and_then(|v| match v {
Value::Static(serde_json::Value::String(s)) => Some(s.clone()),
_ => None,
});
(binding, template, key_path)
};
let array = evaluate_binding(&array_binding, state).unwrap_or(serde_json::Value::Array(vec![]));
let items = match &array {
serde_json::Value::Array(items) => items.clone(),
_ => return,
};
let mut ctx = ReconcileCtx {
tree,
state,
patches,
dependencies,
data_sources,
modules: None,
};
reconcile_iterable_children(
&mut ctx,
node_id,
&items,
"item",
key_path_owned.as_deref(),
&element_template.ir_children,
);
}
#[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());
}
}