feat(hemx): reuse todo row partials

Make the canonical v0 todo flow use one TodoRow partial shape for initial list rendering and generated keyed append/replace/remove helpers. Teach hemx-build to treat collection hosts with keyed h-for children as keyed targets, so beginner templates can keep the DRY outer slot + child partial composition.

req: canonical_authoring/002

req: canonical_authoring/003

req: examples/004

req: list/001
This commit is contained in:
slhx agent
2026-06-05 08:56:36 +02:00
parent cb92803db4
commit 3dfb3b684f
7 changed files with 91 additions and 70 deletions
+38 -2
View File
@@ -175,7 +175,9 @@ impl Resources {
if let Some(name) = static_attr(&node.attrs, "data-hemx-slot") {
reject_unkeyed_loop(surface, node.scope, path, "slot", &name)?;
let keyed = is_inside_keyed_for(surface, node.scope);
let keyed = is_inside_keyed_for(surface, node.scope)
|| (can_host_keyed_collection(tag)
&& has_descendant_keyed_for_scope(surface, node.scope));
let canonical = canonical_symbol(root, path, &name);
self.insert_slot(canonical, name, component.clone(), keyed)?;
}
@@ -1275,6 +1277,40 @@ fn event_ident(name: &str) -> Option<String> {
rust_ident(&name.replace(['-', ':'], "_"))
}
fn can_host_keyed_collection(tag: &str) -> bool {
matches!(
tag,
"ul" | "ol" | "tbody" | "thead" | "tfoot" | "table" | "select" | "datalist"
)
}
fn has_descendant_keyed_for_scope(surface: &SurfaceDocument, scope: ScopeId) -> bool {
surface.scopes.iter().enumerate().any(|(index, current)| {
matches!(
current.kind,
ScopeKind::For {
key_expr: Some(_),
..
}
) && is_descendant_scope(surface, ScopeId(index as u32), scope)
})
}
fn is_descendant_scope(surface: &SurfaceDocument, mut scope: ScopeId, ancestor: ScopeId) -> bool {
loop {
let Some(current) = surface.scopes.get(scope.0 as usize) else {
return false;
};
let Some(parent) = current.parent else {
return false;
};
if parent == ancestor {
return true;
}
scope = parent;
}
}
fn is_inside_keyed_for(surface: &SurfaceDocument, mut scope: ScopeId) -> bool {
loop {
let Some(current) = surface.scopes.get(scope.0 as usize) else {
@@ -1732,7 +1768,7 @@ mod tests {
std::fs::create_dir_all(&templates).unwrap();
std::fs::write(
templates.join("todos.heml"),
r#"<ul data-hemx-slot="row"><template h-for="todo in &self.todos" h-key="todo.id"><li data-hemx-slot="row" +data-key="todo.id">{+ todo.title +}</li></template></ul>"#,
r#"<ul data-hemx-slot="row"><template h-for="todo in &self.todos" h-key="todo.id"><li +data-key="todo.id">{+ todo +}</li></template></ul>"#,
)
.unwrap();