use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use js_sys::{Array, Object, Reflect};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use web_sys::{console, DocumentFragment, Element, HtmlTemplateElement, Node};
use crate::loop_scope::LoopScope;
use crate::mount::{self, bind_scope_to, track_effect_on};
use crate::path::resolve_path;
use crate::reactive::{effect, trigger_scope, ScopeId};
use crate::scope::Scope;
#[derive(Clone)]
pub struct ForTemplate {
anchor: Element,
html: Option<HtmlTemplateElement>,
inline_prototype: Option<Element>,
}
impl ForTemplate {
pub fn from_element(el: Element) -> Option<Self> {
if let Ok(template) = el.clone().dyn_into::<HtmlTemplateElement>() {
return Some(Self {
anchor: el,
html: Some(template),
inline_prototype: None,
});
}
if el.local_name() != "template" {
return None;
}
let inline_prototype = el
.first_element_child()
.and_then(|child| child.clone_node_with_deep(true).ok())
.and_then(|node| node.dyn_into::<Element>().ok());
while let Some(child) = el.first_child() {
let _ = el.remove_child(&child);
}
let _ = el.set_attribute("aria-hidden", "true");
let _ = el.set_attribute("style", "display:none");
Some(Self {
anchor: el,
html: None,
inline_prototype,
})
}
fn anchor(&self) -> Element {
self.anchor.clone()
}
fn clone_body(&self) -> Option<Element> {
if let Some(template) = &self.html {
return clone_template_body(template);
}
self.inline_prototype
.as_ref()?
.clone_node_with_deep(true)
.ok()?
.dyn_into::<Element>()
.ok()
}
}
fn first_layout_child(el: &Element) -> Option<Element> {
let children = el.children();
if children.length() == 0 {
return None;
}
children.item(0)
}
fn retract_from_prior(prior: &Rc<RefCell<Vec<PrevItem>>>, key: &Rc<str>) {
let mut p = prior.borrow_mut();
p.retain(|item| !(Rc::ptr_eq(&item.key, key) && item.leaving));
}
fn fire_staggered_enter(clones: &[Element], stagger_ms: u32) {
if stagger_ms == 0 {
for el in clones {
crate::directives::transition::enter_subtree(el, || {});
}
return;
}
crate::directives::transition::enter_subtrees_sequenced(clones, stagger_ms);
}
fn remove_or_leave(root: &Element) {
if !crate::directives::transition::has_transition_in_subtree(root) {
if let Some(parent) = root.parent_node() {
let _ = parent.remove_child(root);
}
return;
}
let root_cap = root.clone();
crate::directives::transition::leave_subtree(root, move || {
if let Some(parent) = root_cap.parent_node() {
let _ = parent.remove_child(&root_cap);
}
});
}
fn bulk_clear_safe<'a>(
parent_el: &Element,
entries: impl IntoIterator<Item = &'a PrevItem>,
template_el: &Element,
pool_count: usize,
) -> bool {
let parent_node: &Node = parent_el.as_ref();
if parent_el.child_element_count() as usize != pool_count + 1 {
return false;
}
let template_parent_ok = template_el
.parent_node()
.as_ref()
.is_some_and(|parent| parent.is_same_node(Some(parent_node)));
if !template_parent_ok {
return false;
}
for entry in entries {
let row_parent_ok = entry
.element
.parent_node()
.as_ref()
.is_some_and(|parent| parent.is_same_node(Some(parent_node)));
if !row_parent_ok {
return false;
}
}
let nodes = parent_el.child_nodes();
for i in 0..nodes.length() {
let Some(node) = nodes.item(i) else {
return false;
};
match node.node_type() {
Node::ELEMENT_NODE => {}
Node::TEXT_NODE => {
let text = node.text_content().unwrap_or_default();
if !text.chars().all(|c| c.is_whitespace()) {
return false;
}
}
_ => return false,
}
}
true
}
fn bulk_clear_compiled(parent_el: &Element, entries: &[PrevItem], template_el: &Element) -> bool {
let pool_count = entries.len();
if pool_count == 0 || !bulk_clear_safe(parent_el, entries.iter(), template_el, pool_count) {
return false;
}
let scope_ids: Vec<ScopeId> = entries.iter().map(|entry| entry.scope_id).collect();
crate::directives::for_plan::unmount_rows_bulk(&scope_ids);
Scope::remove_compiled_rows(&scope_ids);
parent_el.replace_children_with_node_1(template_el.as_ref());
true
}
fn flip_target_for_entry(entry: &PrevItem) -> Option<Element> {
if !entry.element.is_connected()
|| entry.element.get_attribute("data-pp-animate").as_deref() != Some("flip")
{
return None;
}
Some(first_layout_child(&entry.element).unwrap_or_else(|| entry.element.clone()))
}
fn lift_leaver_out_of_layout(entry: &PrevItem) {
let Some(target) = flip_target_for_entry(entry) else {
return;
};
let rect = target.get_bounding_client_rect();
let Some(html) = target.dyn_ref::<web_sys::HtmlElement>() else {
return;
};
let style = html.style();
let _ = style.set_property("position", "fixed");
let _ = style.set_property("left", &format!("{}px", rect.left()));
let _ = style.set_property("top", &format!("{}px", rect.top()));
let _ = style.set_property("width", &format!("{}px", rect.width()));
let _ = style.set_property("height", &format!("{}px", rect.height()));
let _ = style.set_property("margin", "0");
let _ = style.set_property("z-index", "1");
let _ = style.set_property("pointer-events", "none");
}
fn restore_leaver_layout(entry: &PrevItem) {
let Some(target) = flip_target_for_entry(entry) else {
return;
};
let Some(html) = target.dyn_ref::<web_sys::HtmlElement>() else {
return;
};
let style = html.style();
for prop in [
"position",
"left",
"top",
"width",
"height",
"margin",
"z-index",
"pointer-events",
] {
let _ = style.remove_property(prop);
}
}
#[allow(clippy::too_many_arguments)]
pub fn install(
template: ForTemplate,
parent_proxy: JsValue,
parent_scope_id: ScopeId,
item_name: &'static str,
items_expr: &'static str,
key_expr: Option<&'static str>,
stagger_ms: u32,
body: Option<crate::directives::for_plan::ForBodyFn>,
) {
let template_el = template.anchor();
let track_anchor = template_el.clone();
let inject_parent_id =
crate::mount::inherited_ctx_parent_of(&template_el).unwrap_or(parent_scope_id);
let effect_id = match key_expr {
Some(key) if !key.trim().is_empty() => run_keyed(
item_name,
items_expr,
key,
parent_proxy,
parent_scope_id,
inject_parent_id,
template,
template_el,
stagger_ms,
body,
),
_ => run_naive(
item_name,
items_expr,
parent_proxy,
parent_scope_id,
inject_parent_id,
template,
template_el,
stagger_ms,
body,
),
};
track_effect_on(&track_anchor, effect_id);
}
#[allow(clippy::too_many_arguments)]
fn run_naive(
item_name: &'static str,
items_expr: &'static str,
parent_proxy: JsValue,
parent_scope_id: ScopeId,
inject_parent_id: ScopeId,
template: ForTemplate,
template_el: Element,
stagger_ms: u32,
body: Option<crate::directives::for_plan::ForBodyFn>,
) -> crate::reactive::EffectId {
let item_name: Rc<str> = item_name.into();
let prior: Rc<RefCell<Vec<Element>>> = Rc::new(RefCell::new(Vec::new()));
effect(move || {
let items_js = resolve_path(&parent_proxy, items_expr);
let arr: Array = items_js
.dyn_into::<Array>()
.unwrap_or_else(|_| Array::new());
let total = arr.length() as usize;
{
let mut prior = prior.borrow_mut();
for el in prior.drain(..) {
remove_or_leave(&el);
}
}
if total == 0 {
return;
}
let Some(parent_node) = template_el.parent_node() else {
return;
};
let mut fresh: Vec<Element> = Vec::with_capacity(total);
for i in 0..total {
let item = arr.get(i as u32);
let loop_state = LoopScope {
item_name: Rc::clone(&item_name),
item,
index: i,
total,
parent: parent_proxy.clone(),
parent_scope_id,
};
let scope = Scope::new(Rc::new(RefCell::new(loop_state)));
crate::context::set_parent(scope.id, inject_parent_id);
let proxy = scope.into_proxy();
let (clone_root, fragment_built) = match body {
Some(f) => match f(scope.id, &proxy, scope.id) {
Some(root) => (root, true),
None => {
console::error_1(&JsValue::from_str(
"pp-for: row body fragment failed to materialise root",
));
break;
}
},
None => match template.clone_body() {
Some(root) => (root, false),
None => {
console::error_1(&JsValue::from_str(
"pp-for: <template> body must contain exactly one element",
));
break;
}
},
};
bind_scope_to(&clone_root, scope.id, &proxy);
if parent_node
.insert_before(clone_root.as_ref(), Some(template_el.as_ref()))
.is_ok()
{
if fragment_built {
mount::finalize_compiled_subtree(&clone_root);
} else {
let ctx_key = wasm_bindgen::JsValue::from_str(mount::CTX_PARENT_KEY);
let ctx_val = wasm_bindgen::JsValue::from_f64(scope.id.0 as f64);
let _ = js_sys::Reflect::set(clone_root.as_ref(), &ctx_key, &ctx_val);
mount::finalize_compiled_subtree(&clone_root);
}
fresh.push(clone_root);
}
}
fire_staggered_enter(&fresh, stagger_ms);
*prior.borrow_mut() = fresh;
})
}
struct PrevItem {
element: Element,
scope_id: ScopeId,
loop_state: Rc<RefCell<LoopScope>>,
key: Rc<str>,
item_value: JsValue,
item_sig: String,
leaving: bool,
}
#[allow(clippy::too_many_arguments)]
fn create_keyed_prev_item(
item_name: &Rc<str>,
item: JsValue,
item_sig: String,
index: usize,
total: usize,
parent_proxy: &JsValue,
parent_scope_id: ScopeId,
inject_parent_id: ScopeId,
template: &ForTemplate,
key: Rc<str>,
body: Option<crate::directives::for_plan::ForBodyFn>,
elide_proxy: bool,
) -> Option<PrevItem> {
let loop_rc = Rc::new(RefCell::new(LoopScope {
item_name: Rc::clone(item_name),
item: item.clone(),
index,
total,
parent: parent_proxy.clone(),
parent_scope_id,
}));
let scope = Scope::new(loop_rc.clone());
crate::context::set_parent(scope.id, inject_parent_id);
let clone_start = crate::profiler::mount::start();
let clone_root = if let Some(body_fn) = body {
let proxy = scope.into_proxy();
match body_fn(scope.id, &proxy, scope.id) {
Some(root) => {
bind_scope_to(&root, scope.id, &proxy);
Some(root)
}
None => {
console::error_1(&JsValue::from_str(
"pp-for: row body fragment failed to materialise root",
));
Scope::remove(scope.id);
None
}
}
} else {
None
};
let clone_root = match clone_root {
Some(root) => root,
None => {
let Some(root) = template.clone_body() else {
console::error_1(&JsValue::from_str(
"pp-for: <template> body must contain exactly one element",
));
Scope::remove(scope.id);
return None;
};
if elide_proxy {
mount::bind_scope_id_only(&root, scope.id);
} else {
let proxy = scope.into_proxy();
bind_scope_to(&root, scope.id, &proxy);
}
let ctx_key = wasm_bindgen::JsValue::from_str(mount::CTX_PARENT_KEY);
let ctx_val = wasm_bindgen::JsValue::from_f64(scope.id.0 as f64);
let _ = js_sys::Reflect::set(root.as_ref(), &ctx_key, &ctx_val);
root
}
};
crate::profiler::mount::record_clone_template_body(clone_start);
Some(PrevItem {
element: clone_root,
scope_id: scope.id,
loop_state: loop_rc,
key,
item_value: item,
item_sig,
leaving: false,
})
}
fn item_signature(v: &JsValue) -> String {
if v.is_undefined() {
return "u:".into();
}
if v.is_null() {
return "n:".into();
}
if let Some(s) = v.as_string() {
return format!("s:{s}");
}
if let Some(n) = v.as_f64() {
return format!("f:{n}");
}
if let Some(b) = v.as_bool() {
return if b { "b:1".into() } else { "b:0".into() };
}
js_sys::JSON::stringify(v)
.ok()
.and_then(|s| s.as_string())
.map(|s| format!("j:{s}"))
.unwrap_or_default()
}
#[allow(clippy::too_many_arguments)]
fn try_append_fast_path(
arr: &Array,
total: usize,
mut old_prior: Vec<PrevItem>,
seen_cell: &Rc<RefCell<HashSet<Rc<str>>>>,
key_resolver: &KeyResolver,
item_name: &Rc<str>,
parent_proxy: &JsValue,
parent_scope_id: ScopeId,
inject_parent_id: ScopeId,
template: &ForTemplate,
template_el: &Element,
parent_node: &Node,
body: Option<crate::directives::for_plan::ForBodyFn>,
elide_proxy: bool,
row_plan: Option<&Rc<crate::directives::for_plan::CompiledRowPlan>>,
compiled_bindings_depend_on_position: bool,
stagger_ms: u32,
) -> Result<Vec<PrevItem>, Vec<PrevItem>> {
let old_len = old_prior.len();
if old_len == 0 || total <= old_len {
return Err(old_prior);
}
for (i, entry) in old_prior.iter().enumerate() {
if entry.leaving || !Object::is(&entry.item_value, &arr.get(i as u32)) {
return Err(old_prior);
}
}
let row_iter_start = crate::profiler::reconcile::start();
if compiled_bindings_depend_on_position {
for entry in &old_prior {
{
let mut st = entry.loop_state.borrow_mut();
st.total = total;
}
if !crate::directives::for_plan::reuse_row_compiled(entry.scope_id) {
trigger_scope(entry.scope_id);
}
}
} else {
for entry in &old_prior {
entry.loop_state.borrow_mut().total = total;
trigger_scope(entry.scope_id);
}
}
let mut seen = seen_cell.borrow_mut();
seen.clear();
let seen_cap = seen.capacity();
if seen_cap < total {
seen.reserve(total - seen_cap);
}
for entry in &old_prior {
seen.insert(entry.key.clone());
}
let mut appended: Vec<PrevItem> = Vec::with_capacity(total - old_len);
let create_body = if row_plan.is_none() { body } else { None };
for i in old_len..total {
let item = arr.get(i as u32);
let key_val = key_resolver.resolve(&item, i, parent_proxy);
let raw_key: Rc<str> = stringify_key(&key_val).into();
let key = if seen.insert(raw_key.clone()) {
raw_key
} else {
console::warn_1(&JsValue::from_str(&format!(
"pp-for: duplicate pp-key {:?} at index {i}; treating as new",
&*raw_key
)));
let dup: Rc<str> = format!("{}__dup_{i}", &*raw_key).into();
seen.insert(dup.clone());
dup
};
let Some(entry) = create_keyed_prev_item(
item_name,
item,
String::new(),
i,
total,
parent_proxy,
parent_scope_id,
inject_parent_id,
template,
key,
create_body,
elide_proxy,
) else {
return Err(old_prior);
};
appended.push(entry);
}
drop(seen);
crate::profiler::reconcile::record_row_iter(row_iter_start);
let reorder_start = crate::profiler::reconcile::start();
let doc = template_el
.owner_document()
.expect("template element should belong to a document");
let fragment: DocumentFragment = doc.create_document_fragment();
let dom_insert_start = crate::profiler::mount::start();
for entry in &appended {
let _ = fragment.append_child(entry.element.as_ref());
}
let _ = parent_node.insert_before(fragment.as_ref(), Some(template_el.as_ref()));
crate::profiler::mount::record_dom_insertion(dom_insert_start);
for entry in &appended {
if let Some(plan) = row_plan {
if let Some(sid) = mount::scope_id_of_element(&entry.element) {
let proxy_for_mount = if elide_proxy {
None
} else {
crate::mount::scope_of_element(&entry.element).map(|(_, p)| p)
};
crate::directives::for_plan::mount_row_compiled(
plan,
&entry.element,
sid,
proxy_for_mount.as_ref(),
);
continue;
}
}
crate::profiler::mount::record_generic_row_mounted();
mount::finalize_compiled_subtree(&entry.element);
}
let entered = appended
.iter()
.map(|entry| entry.element.clone())
.collect::<Vec<_>>();
fire_staggered_enter(&entered, stagger_ms);
crate::profiler::reconcile::record_reorder(reorder_start);
old_prior.extend(appended);
Ok(old_prior)
}
#[allow(clippy::too_many_arguments)]
fn try_prepend_fast_path(
arr: &Array,
total: usize,
old_prior: Vec<PrevItem>,
seen_cell: &Rc<RefCell<HashSet<Rc<str>>>>,
key_resolver: &KeyResolver,
item_name: &Rc<str>,
parent_proxy: &JsValue,
parent_scope_id: ScopeId,
inject_parent_id: ScopeId,
template: &ForTemplate,
parent_node: &Node,
body: Option<crate::directives::for_plan::ForBodyFn>,
elide_proxy: bool,
row_plan: Option<&Rc<crate::directives::for_plan::CompiledRowPlan>>,
compiled_bindings_depend_on_position: bool,
stagger_ms: u32,
) -> Result<Vec<PrevItem>, Vec<PrevItem>> {
let old_len = old_prior.len();
if old_len == 0 || total <= old_len {
return Err(old_prior);
}
let added = total - old_len;
for (i, entry) in old_prior.iter().enumerate() {
if entry.leaving || !Object::is(&entry.item_value, &arr.get((i + added) as u32)) {
return Err(old_prior);
}
}
let row_iter_start = crate::profiler::reconcile::start();
if compiled_bindings_depend_on_position {
for (i, entry) in old_prior.iter().enumerate() {
{
let mut st = entry.loop_state.borrow_mut();
st.index = i + added;
st.total = total;
}
if !crate::directives::for_plan::reuse_row_compiled(entry.scope_id) {
trigger_scope(entry.scope_id);
}
}
} else {
for (i, entry) in old_prior.iter().enumerate() {
let mut st = entry.loop_state.borrow_mut();
st.index = i + added;
st.total = total;
drop(st);
trigger_scope(entry.scope_id);
}
}
let mut seen = seen_cell.borrow_mut();
seen.clear();
let seen_cap = seen.capacity();
if seen_cap < total {
seen.reserve(total - seen_cap);
}
for entry in &old_prior {
seen.insert(entry.key.clone());
}
let create_body = if row_plan.is_none() { body } else { None };
let mut prepended: Vec<PrevItem> = Vec::with_capacity(added);
for i in 0..added {
let item = arr.get(i as u32);
let key_val = key_resolver.resolve(&item, i, parent_proxy);
let raw_key: Rc<str> = stringify_key(&key_val).into();
let key = if seen.insert(raw_key.clone()) {
raw_key
} else {
console::warn_1(&JsValue::from_str(&format!(
"pp-for: duplicate pp-key {:?} at index {i}; treating as new",
&*raw_key
)));
let dup: Rc<str> = format!("{}__dup_{i}", &*raw_key).into();
seen.insert(dup.clone());
dup
};
let Some(entry) = create_keyed_prev_item(
item_name,
item,
String::new(),
i,
total,
parent_proxy,
parent_scope_id,
inject_parent_id,
template,
key,
create_body,
elide_proxy,
) else {
return Err(old_prior);
};
prepended.push(entry);
}
drop(seen);
crate::profiler::reconcile::record_row_iter(row_iter_start);
let reorder_start = crate::profiler::reconcile::start();
let doc = old_prior
.first()
.and_then(|entry| entry.element.owner_document())
.expect("existing row should belong to a document");
let fragment: DocumentFragment = doc.create_document_fragment();
let dom_insert_start = crate::profiler::mount::start();
for entry in &prepended {
let _ = fragment.append_child(entry.element.as_ref());
}
let anchor = old_prior
.first()
.map(|entry| entry.element.as_ref() as &Node);
let _ = parent_node.insert_before(fragment.as_ref(), anchor);
crate::profiler::mount::record_dom_insertion(dom_insert_start);
for entry in &prepended {
if let Some(plan) = row_plan {
if let Some(sid) = mount::scope_id_of_element(&entry.element) {
let proxy_for_mount = if elide_proxy {
None
} else {
crate::mount::scope_of_element(&entry.element).map(|(_, p)| p)
};
crate::directives::for_plan::mount_row_compiled(
plan,
&entry.element,
sid,
proxy_for_mount.as_ref(),
);
continue;
}
}
crate::profiler::mount::record_generic_row_mounted();
mount::finalize_compiled_subtree(&entry.element);
}
let entered = prepended
.iter()
.map(|entry| entry.element.clone())
.collect::<Vec<_>>();
fire_staggered_enter(&entered, stagger_ms);
crate::profiler::reconcile::record_reorder(reorder_start);
prepended.extend(old_prior);
Ok(prepended)
}
fn update_reused_entry(
entry: &mut PrevItem,
index: usize,
total: usize,
compiled_bindings_depend_on_position: bool,
) {
let position_changed = {
let st = entry.loop_state.borrow();
st.index != index || st.total != total
};
if !position_changed {
return;
}
{
let mut st = entry.loop_state.borrow_mut();
st.index = index;
st.total = total;
}
if compiled_bindings_depend_on_position
&& !crate::directives::for_plan::reuse_row_compiled(entry.scope_id)
{
trigger_scope(entry.scope_id);
}
}
#[allow(clippy::too_many_arguments)]
fn try_single_remove_fast_path(
arr: &Array,
total: usize,
old_prior: Vec<PrevItem>,
row_plan: Option<&Rc<crate::directives::for_plan::CompiledRowPlan>>,
compiled_bindings_depend_on_position: bool,
) -> Result<Vec<PrevItem>, Vec<PrevItem>> {
let old_len = old_prior.len();
if old_len == 0 || old_len != total + 1 {
return Err(old_prior);
}
if row_plan.is_none() {
return Err(old_prior);
}
let mut removed_idx: Option<usize> = None;
let mut new_i = 0usize;
for old_i in 0..old_len {
if new_i < total && Object::is(&old_prior[old_i].item_value, &arr.get(new_i as u32)) {
new_i += 1;
continue;
}
if removed_idx.is_some() {
return Err(old_prior);
}
removed_idx = Some(old_i);
}
if new_i != total {
return Err(old_prior);
}
let Some(removed_idx) = removed_idx else {
return Err(old_prior);
};
if old_prior[removed_idx].leaving
|| crate::directives::transition::has_transition_in_subtree(&old_prior[removed_idx].element)
{
return Err(old_prior);
}
let row_iter_start = crate::profiler::reconcile::start();
let mut fresh = Vec::with_capacity(total);
let mut removed: Option<PrevItem> = None;
for (old_i, mut entry) in old_prior.into_iter().enumerate() {
if old_i == removed_idx {
removed = Some(entry);
continue;
}
let index = fresh.len();
update_reused_entry(
&mut entry,
index,
total,
compiled_bindings_depend_on_position,
);
fresh.push(entry);
}
crate::profiler::reconcile::record_row_iter(row_iter_start);
let leaver_drain_start = crate::profiler::reconcile::start();
if let Some(entry) = removed {
if !entry.leaving {
if let Some(parent) = entry.element.parent_node() {
let _ = parent.remove_child(&entry.element);
}
crate::directives::for_plan::unmount_row_compiled(entry.scope_id);
}
}
crate::profiler::reconcile::record_leaver_drain(leaver_drain_start);
Ok(fresh)
}
#[allow(clippy::too_many_arguments)]
fn try_two_swap_fast_path(
arr: &Array,
total: usize,
mut old_prior: Vec<PrevItem>,
parent_node: &Node,
row_plan: Option<&Rc<crate::directives::for_plan::CompiledRowPlan>>,
compiled_bindings_depend_on_position: bool,
) -> Result<Vec<PrevItem>, Vec<PrevItem>> {
if total < 2 || old_prior.len() != total {
return Err(old_prior);
}
if row_plan.is_none() {
return Err(old_prior);
}
if old_prior.iter().any(|entry| entry.leaving) {
return Err(old_prior);
}
let mut mismatches: Vec<usize> = Vec::with_capacity(2);
for i in 0..total {
if !Object::is(&old_prior[i].item_value, &arr.get(i as u32)) {
mismatches.push(i);
if mismatches.len() > 2 {
return Err(old_prior);
}
}
}
if mismatches.len() != 2 {
return Err(old_prior);
}
let a = mismatches[0];
let b = mismatches[1];
if !Object::is(&old_prior[a].item_value, &arr.get(b as u32))
|| !Object::is(&old_prior[b].item_value, &arr.get(a as u32))
{
return Err(old_prior);
}
let row_iter_start = crate::profiler::reconcile::start();
old_prior.swap(a, b);
update_reused_entry(
&mut old_prior[a],
a,
total,
compiled_bindings_depend_on_position,
);
update_reused_entry(
&mut old_prior[b],
b,
total,
compiled_bindings_depend_on_position,
);
crate::profiler::reconcile::record_row_iter(row_iter_start);
let reorder_start = crate::profiler::reconcile::start();
let dom_insert_start = crate::profiler::mount::start();
let moved_to_front = old_prior[a].element.clone();
let moved_to_back = old_prior[b].element.clone();
let back_next = moved_to_front.next_sibling();
let _ = parent_node.insert_before(moved_to_front.as_ref(), Some(moved_to_back.as_ref()));
let _ = parent_node.insert_before(moved_to_back.as_ref(), back_next.as_ref());
crate::profiler::mount::record_dom_insertion(dom_insert_start);
crate::profiler::reconcile::record_reorder(reorder_start);
Ok(old_prior)
}
#[allow(clippy::too_many_arguments)]
fn run_keyed(
item_name: &'static str,
items_expr: &'static str,
key_expr: &'static str,
parent_proxy: JsValue,
parent_scope_id: ScopeId,
inject_parent_id: ScopeId,
template: ForTemplate,
template_el: Element,
stagger_ms: u32,
body: Option<crate::directives::for_plan::ForBodyFn>,
) -> crate::reactive::EffectId {
let key_resolver = KeyResolver::parse(item_name, key_expr);
let row_plan: Option<Rc<crate::directives::for_plan::CompiledRowPlan>> =
crate::directives::for_plan::lookup_for_template(&template_el);
let elide_proxy = row_plan
.as_ref()
.map(|p| p.is_proxy_elision_eligible())
.unwrap_or(false);
let compiled_bindings_depend_on_position = row_plan
.as_ref()
.map(|p| p.depends_on_loop_position())
.unwrap_or(true);
let item_name: Rc<str> = item_name.into();
let prior: Rc<RefCell<Vec<PrevItem>>> = Rc::new(RefCell::new(Vec::new()));
let pool_cell: Rc<RefCell<HashMap<Rc<str>, PrevItem>>> = Rc::new(RefCell::new(HashMap::new()));
let seen_cell: Rc<RefCell<HashSet<Rc<str>>>> = Rc::new(RefCell::new(HashSet::new()));
effect(move || {
let reconcile_total_start = crate::profiler::reconcile::start();
let items_js = resolve_path(&parent_proxy, items_expr);
let arr: Array = items_js
.dyn_into::<Array>()
.unwrap_or_else(|_| Array::new());
let total = arr.length() as usize;
let Some(parent_node) = template_el.parent_node() else {
prior.borrow_mut().clear();
crate::profiler::reconcile::record_total(reconcile_total_start);
return;
};
let parent_node_ref: &Node = parent_node.as_ref();
if let Some(plan) = row_plan.as_ref() {
crate::directives::for_plan::ensure_delegated_listeners(
plan,
&template_el,
parent_node_ref,
);
}
let old_prior: Vec<PrevItem> = {
let mut b = prior.borrow_mut();
std::mem::take(&mut *b)
};
let old_prior = match try_append_fast_path(
&arr,
total,
old_prior,
&seen_cell,
&key_resolver,
&item_name,
&parent_proxy,
parent_scope_id,
inject_parent_id,
&template,
&template_el,
parent_node_ref,
body,
elide_proxy,
row_plan.as_ref(),
compiled_bindings_depend_on_position,
stagger_ms,
) {
Ok(next_prior) => {
*prior.borrow_mut() = next_prior;
crate::profiler::reconcile::record_total(reconcile_total_start);
return;
}
Err(old_prior) => old_prior,
};
let old_prior = match try_prepend_fast_path(
&arr,
total,
old_prior,
&seen_cell,
&key_resolver,
&item_name,
&parent_proxy,
parent_scope_id,
inject_parent_id,
&template,
parent_node_ref,
body,
elide_proxy,
row_plan.as_ref(),
compiled_bindings_depend_on_position,
stagger_ms,
) {
Ok(next_prior) => {
*prior.borrow_mut() = next_prior;
crate::profiler::reconcile::record_total(reconcile_total_start);
return;
}
Err(old_prior) => old_prior,
};
let old_prior = match try_single_remove_fast_path(
&arr,
total,
old_prior,
row_plan.as_ref(),
compiled_bindings_depend_on_position,
) {
Ok(next_prior) => {
*prior.borrow_mut() = next_prior;
crate::profiler::reconcile::record_total(reconcile_total_start);
return;
}
Err(old_prior) => old_prior,
};
let old_prior = match try_two_swap_fast_path(
&arr,
total,
old_prior,
parent_node_ref,
row_plan.as_ref(),
compiled_bindings_depend_on_position,
) {
Ok(next_prior) => {
*prior.borrow_mut() = next_prior;
crate::profiler::reconcile::record_total(reconcile_total_start);
return;
}
Err(old_prior) => old_prior,
};
let pool_build_start = crate::profiler::reconcile::start();
let mut pool = pool_cell.borrow_mut();
pool.clear();
let pool_cap = pool.capacity();
if pool_cap < total {
pool.reserve(total - pool_cap);
}
if total == 0 && row_plan.is_some() {
if let Some(parent_el) = parent_node.dyn_ref::<Element>() {
crate::profiler::reconcile::record_pool_build(pool_build_start);
let leaver_drain_start = crate::profiler::reconcile::start();
if bulk_clear_compiled(parent_el, &old_prior, &template_el) {
prior.borrow_mut().clear();
crate::profiler::reconcile::record_leaver_drain(leaver_drain_start);
crate::profiler::reconcile::record_total(reconcile_total_start);
return;
}
}
}
for entry in old_prior {
pool.insert(entry.key.clone(), entry);
}
let mut seen = seen_cell.borrow_mut();
seen.clear();
let seen_cap = seen.capacity();
if seen_cap < total {
seen.reserve(total - seen_cap);
}
crate::profiler::reconcile::record_pool_build(pool_build_start);
let mut fresh: Vec<PrevItem> = Vec::with_capacity(total);
let pool_initially_empty = pool.is_empty();
let row_iter_start = crate::profiler::reconcile::start();
for i in 0..total {
let item = arr.get(i as u32);
let key_val = key_resolver.resolve(&item, i, &parent_proxy);
let raw_key: Rc<str> = stringify_key(&key_val).into();
let item_sig: String = if pool_initially_empty {
String::new()
} else {
String::new()
};
let key: Rc<str> = if seen.insert(raw_key.clone()) {
raw_key
} else {
console::warn_1(&JsValue::from_str(&format!(
"pp-for: duplicate pp-key {:?} at index {i}; treating as new",
&*raw_key
)));
let dup: Rc<str> = format!("{}__dup_{i}", &*raw_key).into();
seen.insert(dup.clone());
dup
};
let pool_lookup = if pool_initially_empty {
None
} else {
pool.remove(&key)
};
if let Some(mut entry) = pool_lookup {
if entry.leaving {
restore_leaver_layout(&entry);
crate::directives::transition::enter_subtree(&entry.element, || {});
entry.leaving = false;
}
let same_item = Object::is(&entry.item_value, &item);
let mut next_item_sig = entry.item_sig.clone();
let item_changed = if same_item {
false
} else {
next_item_sig = item_signature(&item);
next_item_sig != entry.item_sig
};
let position_changed = {
let st = entry.loop_state.borrow();
st.index != i || st.total != total
};
let needs_loop_update = position_changed || !same_item;
let needs_binding_update = item_changed
|| (position_changed
&& (row_plan.is_none() || compiled_bindings_depend_on_position));
if needs_loop_update {
let item_for_entry = item.clone();
{
let mut st = entry.loop_state.borrow_mut();
st.item = item;
st.index = i;
st.total = total;
}
entry.item_value = item_for_entry;
entry.item_sig = next_item_sig;
}
if needs_binding_update {
if !crate::directives::for_plan::reuse_row_compiled(entry.scope_id) {
trigger_scope(entry.scope_id);
}
}
fresh.push(entry);
} else {
let item_sig = if pool_initially_empty {
item_sig
} else {
item_signature(&item)
};
let loop_rc = Rc::new(RefCell::new(LoopScope {
item_name: Rc::clone(&item_name),
item: item.clone(),
index: i,
total,
parent: parent_proxy.clone(),
parent_scope_id,
}));
let scope = Scope::new(loop_rc.clone());
crate::context::set_parent(scope.id, inject_parent_id);
let clone_start = crate::profiler::mount::start();
let clone_root = if row_plan.is_none() {
if let Some(body_fn) = body {
let proxy = scope.into_proxy();
match body_fn(scope.id, &proxy, scope.id) {
Some(root) => {
bind_scope_to(&root, scope.id, &proxy);
Some(root)
}
None => {
console::error_1(&JsValue::from_str(
"pp-for: row body fragment failed to materialise root",
));
Scope::remove(scope.id);
break;
}
}
} else {
None
}
} else {
None
};
let clone_root = match clone_root {
Some(root) => root,
None => {
let Some(root) = template.clone_body() else {
console::error_1(&JsValue::from_str(
"pp-for: <template> body must contain exactly one element",
));
Scope::remove(scope.id);
break;
};
if elide_proxy {
mount::bind_scope_id_only(&root, scope.id);
} else {
let proxy = scope.into_proxy();
bind_scope_to(&root, scope.id, &proxy);
}
let ctx_key = wasm_bindgen::JsValue::from_str(mount::CTX_PARENT_KEY);
let ctx_val = wasm_bindgen::JsValue::from_f64(scope.id.0 as f64);
let _ = js_sys::Reflect::set(root.as_ref(), &ctx_key, &ctx_val);
root
}
};
crate::profiler::mount::record_clone_template_body(clone_start);
fresh.push(PrevItem {
element: clone_root,
scope_id: scope.id,
loop_state: loop_rc,
key,
item_value: item,
item_sig,
leaving: false,
});
}
}
crate::profiler::reconcile::record_row_iter(row_iter_start);
let mut leaver_flip_snapshots: HashMap<Rc<str>, (Element, web_sys::DomRect)> =
if pool.is_empty() {
HashMap::new()
} else {
let mut snapshots = HashMap::new();
for entry in &fresh {
let Some(target) = flip_target_for_entry(entry) else {
continue;
};
snapshots.insert(
entry.key.clone(),
(target.clone(), target.get_bounding_client_rect()),
);
}
snapshots
};
let has_leavers = !pool.is_empty();
let n_new: usize = fresh.len();
let mut leavers: Vec<PrevItem> = Vec::new();
let leaver_drain_start = crate::profiler::reconcile::start();
if total == 0 && !pool.is_empty() && row_plan.is_some() {
if let Some(parent_el) = parent_node.dyn_ref::<Element>() {
let pool_count = pool.len();
if bulk_clear_safe(parent_el, pool.values(), &template_el, pool_count) {
let scope_ids: Vec<ScopeId> =
pool.values().map(|entry| entry.scope_id).collect();
crate::directives::for_plan::unmount_rows_bulk(&scope_ids);
Scope::remove_compiled_rows(&scope_ids);
pool.clear();
parent_el.replace_children_with_node_1(template_el.as_ref());
prior.borrow_mut().clear();
crate::profiler::reconcile::record_leaver_drain(leaver_drain_start);
crate::profiler::reconcile::record_total(reconcile_total_start);
return;
}
}
}
for (_, mut entry) in pool.drain() {
if !entry.leaving {
entry.leaving = true;
lift_leaver_out_of_layout(&entry);
let el = entry.element.clone();
let el_for_cb = el.clone();
let scope_id_for_unmount = entry.scope_id;
let key_for_retract = Rc::clone(&entry.key);
let prior_for_retract = prior.clone();
if !crate::directives::transition::has_transition_in_subtree(&el) {
if let Some(parent) = el.parent_node() {
let _ = parent.remove_child(&el);
}
crate::directives::for_plan::unmount_row_compiled(scope_id_for_unmount);
retract_from_prior(&prior_for_retract, &key_for_retract);
continue;
}
crate::directives::transition::leave_subtree(&el, move || {
if let Some(parent) = el_for_cb.parent_node() {
let _ = parent.remove_child(&el_for_cb);
}
crate::directives::for_plan::unmount_row_compiled(scope_id_for_unmount);
retract_from_prior(&prior_for_retract, &key_for_retract);
});
}
leavers.push(entry);
}
crate::profiler::reconcile::record_leaver_drain(leaver_drain_start);
fn next_non_leaving(node: Option<web_sys::Node>) -> Option<web_sys::Node> {
let mut cursor = node;
while let Some(n) = cursor.clone() {
if let Ok(el) = n.dyn_into::<Element>() {
if crate::directives::transition::is_leaving(&el) {
cursor = el.next_sibling();
continue;
}
}
return cursor;
}
None
}
let already_ordered = fresh.iter().enumerate().all(|(i, entry)| {
let correct_parent = entry
.element
.parent_node()
.map(|p| p.is_same_node(Some(parent_node_ref)))
.unwrap_or(false);
let expected_next: &Node = if i + 1 < fresh.len() {
fresh[i + 1].element.as_ref()
} else {
template_el.as_ref()
};
let correct_next = next_non_leaving(entry.element.next_sibling())
.map(|n| n.is_same_node(Some(expected_next)))
.unwrap_or(false);
correct_parent && correct_next
});
let mut flip_snapshots: HashMap<Rc<str>, (Element, web_sys::DomRect)> = HashMap::new();
if has_leavers {
flip_snapshots = std::mem::take(&mut leaver_flip_snapshots);
} else if !already_ordered {
for entry in &fresh {
let Some(target) = flip_target_for_entry(entry) else {
continue;
};
flip_snapshots.insert(
entry.key.clone(),
(target.clone(), target.get_bounding_client_rect()),
);
}
}
let mut newly_walked: Vec<Element> = Vec::new();
let reorder_start = crate::profiler::reconcile::start();
if !already_ordered {
let suffix_insert_start = fresh
.iter()
.position(|entry| entry.element.parent_node().is_none())
.unwrap_or(fresh.len());
let can_batch_suffix = !has_leavers
&& suffix_insert_start < fresh.len()
&& fresh[suffix_insert_start..]
.iter()
.all(|entry| entry.element.parent_node().is_none())
&& fresh[..suffix_insert_start]
.iter()
.enumerate()
.all(|(i, entry)| {
let correct_parent = entry
.element
.parent_node()
.map(|p| p.is_same_node(Some(parent_node_ref)))
.unwrap_or(false);
let expected_next: &Node = if i + 1 < fresh.len() {
fresh[i + 1].element.as_ref()
} else {
template_el.as_ref()
};
let correct_next = next_non_leaving(entry.element.next_sibling())
.map(|n| n.is_same_node(Some(expected_next)))
.unwrap_or(false);
correct_parent && correct_next
});
if can_batch_suffix {
let doc = template_el
.owner_document()
.expect("template element should belong to a document");
let fragment: DocumentFragment = doc.create_document_fragment();
let dom_insert_start = crate::profiler::mount::start();
for entry in &fresh[suffix_insert_start..] {
let _ = fragment.append_child(entry.element.as_ref());
newly_walked.push(entry.element.clone());
}
let _ = parent_node.insert_before(fragment.as_ref(), Some(template_el.as_ref()));
crate::profiler::mount::record_dom_insertion(dom_insert_start);
} else {
let mut new_indices: Vec<usize> = Vec::new();
let dom_insert_start = crate::profiler::mount::start();
for i in (0..fresh.len()).rev() {
let entry = &fresh[i];
let was_in_place = entry
.element
.parent_node()
.map(|p| p.is_same_node(Some(parent_node_ref)))
.unwrap_or(false);
let anchor: &Node = if i + 1 < fresh.len() {
fresh[i + 1].element.as_ref()
} else {
template_el.as_ref()
};
let already_here = was_in_place
&& next_non_leaving(entry.element.next_sibling())
.map(|n| n.is_same_node(Some(anchor)))
.unwrap_or(false);
if !already_here {
let _ = parent_node.insert_before(entry.element.as_ref(), Some(anchor));
}
if !was_in_place {
new_indices.push(i);
}
}
crate::profiler::mount::record_dom_insertion(dom_insert_start);
new_indices.sort_unstable();
newly_walked.extend(new_indices.into_iter().map(|i| fresh[i].element.clone()));
}
}
if let Some(plan) = &row_plan {
let mut compiled_rows: Vec<(Element, ScopeId, Option<JsValue>)> =
Vec::with_capacity(newly_walked.len());
let mut generic_rows: Vec<Element> = Vec::new();
for el in &newly_walked {
if let Some(sid) = mount::scope_id_of_element(el) {
let proxy_for_mount = if elide_proxy {
None
} else {
crate::mount::scope_of_element(el).map(|(_, p)| p)
};
compiled_rows.push((el.clone(), sid, proxy_for_mount));
} else {
generic_rows.push(el.clone());
}
}
crate::directives::for_plan::mount_rows_compiled(plan, &compiled_rows);
for el in &generic_rows {
crate::profiler::mount::record_generic_row_mounted();
mount::finalize_compiled_subtree(el);
}
} else {
for el in &newly_walked {
crate::profiler::mount::record_generic_row_mounted();
mount::finalize_compiled_subtree(el);
}
}
fire_staggered_enter(&newly_walked, stagger_ms);
if !flip_snapshots.is_empty() {
let mut pending: Vec<crate::animate::FlipTarget> =
Vec::with_capacity(flip_snapshots.len());
for entry in &fresh {
if let Some((target, old_rect)) = flip_snapshots.remove(&entry.key) {
let new_rect = target.get_bounding_client_rect();
pending.push(crate::animate::FlipTarget {
element: target,
old_rect,
new_rect,
});
}
}
crate::animate::flip_batch(pending, crate::animate::FlipOptions::default());
}
crate::profiler::reconcile::record_reorder(reorder_start);
let _ = n_new; fresh.extend(leavers);
*prior.borrow_mut() = fresh;
crate::profiler::reconcile::record_total(reconcile_total_start);
})
}
enum KeyResolver {
Index,
Item,
ItemField(JsValue),
ItemPath(Vec<JsValue>),
External(String),
}
impl KeyResolver {
fn parse(item_name: &str, expr: &str) -> Self {
let trimmed = expr.trim();
if trimmed == "$index" {
return Self::Index;
}
if trimmed == item_name {
return Self::Item;
}
let prefix_len = item_name.len() + 1;
if trimmed.len() > prefix_len
&& trimmed.starts_with(item_name)
&& trimmed.as_bytes().get(item_name.len()) == Some(&b'.')
{
let rest = &trimmed[prefix_len..];
if !rest.contains('.') && !rest.is_empty() {
return Self::ItemField(JsValue::from_str(rest));
}
return Self::ItemPath(
rest.split('.')
.filter(|s| !s.is_empty())
.map(JsValue::from_str)
.collect(),
);
}
Self::External(trimmed.to_string())
}
fn resolve(&self, item: &JsValue, index: usize, parent_proxy: &JsValue) -> JsValue {
match self {
Self::Index => JsValue::from_f64(index as f64),
Self::Item => item.clone(),
Self::ItemField(field) => Reflect::get(item, field).unwrap_or(JsValue::UNDEFINED),
Self::ItemPath(segments) => segments.iter().fold(item.clone(), |acc, segment| {
Reflect::get(&acc, segment).unwrap_or(JsValue::UNDEFINED)
}),
Self::External(path) => resolve_path(parent_proxy, path),
}
}
}
fn stringify_key(v: &JsValue) -> String {
if v.is_undefined() || v.is_null() {
return String::new();
}
if let Some(s) = v.as_string() {
return s;
}
if let Some(n) = v.as_f64() {
return n.to_string();
}
if let Some(b) = v.as_bool() {
return b.to_string();
}
js_sys::JSON::stringify(v)
.ok()
.and_then(|s| s.as_string())
.unwrap_or_default()
}
fn clone_template_body(template: &HtmlTemplateElement) -> Option<Element> {
template
.content()
.first_element_child()?
.clone_node_with_deep(true)
.ok()?
.dyn_into::<Element>()
.ok()
}
#[cfg(test)]
fn parse_expr(s: &str) -> Option<(String, String)> {
let s = s.trim();
let (lhs, rhs) = s.split_once(" in ")?;
let ident = lhs.trim();
let items = rhs.trim();
if ident.is_empty() || items.is_empty() {
return None;
}
if !ident.chars().all(|c| c.is_alphanumeric() || c == '_')
|| ident.chars().next().is_some_and(|c| c.is_ascii_digit())
{
return None;
}
Some((ident.to_string(), items.to_string()))
}
#[cfg(test)]
mod tests {
use super::parse_expr;
#[test]
fn basic() {
assert_eq!(
parse_expr("story in stories"),
Some(("story".into(), "stories".into()))
);
}
#[test]
fn dotted_path_on_rhs() {
assert_eq!(
parse_expr("child in node.children"),
Some(("child".into(), "node.children".into()))
);
}
#[test]
fn strip_whitespace() {
assert_eq!(
parse_expr(" foo in bar "),
Some(("foo".into(), "bar".into()))
);
}
#[test]
fn rejects_destructuring() {
assert_eq!(parse_expr("(item, i) in items"), None);
}
#[test]
fn rejects_leading_digit() {
assert_eq!(parse_expr("1x in items"), None);
}
#[test]
fn rejects_missing_in() {
assert_eq!(parse_expr("story"), None);
}
}