fix(build): explain missing keyed loop facts
Improve the unkeyed h-for diagnostic so authors see the exact loop, stable h-key shape, generated helper consequence, and why dynamic child data-key is not enough. req: scope/001 req: diagnostics/002
This commit is contained in:
+19
-6
@@ -1543,11 +1543,16 @@ fn reject_unkeyed_loop(
|
|||||||
let Some(current) = surface.scopes.get(scope.0 as usize) else {
|
let Some(current) = surface.scopes.get(scope.0 as usize) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if matches!(current.kind, ScopeKind::For { key_expr: None, .. }) {
|
if let ScopeKind::For {
|
||||||
|
pattern,
|
||||||
|
expr,
|
||||||
|
key_expr: None,
|
||||||
|
} = ¤t.kind
|
||||||
|
{
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidData,
|
io::ErrorKind::InvalidData,
|
||||||
format!(
|
format!(
|
||||||
"{}: data-hemx-{kind}=\"{name}\" is inside an h-for without h-key; add a stable h-key=\"item.id\" to the loop that owns this generated target",
|
"{}: data-hemx-{kind}=\"{name}\" is inside h-for=\"{pattern} in {expr}\" without h-key; add a stable h-key=\"{pattern}.id\" to that h-for so generated keyed helpers such as ui::{name}.replace(row) can target this partial. Dynamic +data-key on the child is rendered HTML, not the template fact hemx uses for generated targets.",
|
||||||
path.display()
|
path.display()
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -2052,22 +2057,25 @@ fn main() {{
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_hemx_resources_inside_unkeyed_for() {
|
fn rejects_hemx_resources_inside_unkeyed_for() {
|
||||||
// req: scope/001
|
// req: scope/001 req: diagnostics/002
|
||||||
for (case, template, resource) in [
|
for (case, template, resource, helper) in [
|
||||||
(
|
(
|
||||||
"slot",
|
"slot",
|
||||||
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row">{+ todo.title +}</li></template>"#,
|
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row">{+ todo.title +}</li></template>"#,
|
||||||
"data-hemx-slot=\"todo_row\"",
|
"data-hemx-slot=\"todo_row\"",
|
||||||
|
"ui::todo_row",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"slot_data_key",
|
"slot_data_key",
|
||||||
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row" +data-key="todo.id">{+ todo.title +}</li></template>"#,
|
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row" +data-key="todo.id">{+ todo.title +}</li></template>"#,
|
||||||
"data-hemx-slot=\"todo_row\"",
|
"data-hemx-slot=\"todo_row\"",
|
||||||
|
"ui::todo_row",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"handle",
|
"handle",
|
||||||
r#"<template h-for="todo in &self.todos"><button data-hemx-handle="delete">Delete</button></template>"#,
|
r#"<template h-for="todo in &self.todos"><button data-hemx-handle="delete">Delete</button></template>"#,
|
||||||
"data-hemx-handle=\"delete\"",
|
"data-hemx-handle=\"delete\"",
|
||||||
|
"ui::delete",
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
let base = test_dir(&format!("hemx-build-unkeyed-for-{case}-test"));
|
let base = test_dir(&format!("hemx-build-unkeyed-for-{case}-test"));
|
||||||
@@ -2082,8 +2090,13 @@ fn main() {{
|
|||||||
.out_dir(&out)
|
.out_dir(&out)
|
||||||
.run()
|
.run()
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(err.to_string().contains("inside an h-for without h-key"));
|
let err = err.to_string();
|
||||||
assert!(err.to_string().contains(resource));
|
assert!(err.contains("inside h-for=\"todo in &self.todos\" without h-key"));
|
||||||
|
assert!(err.contains(resource));
|
||||||
|
assert!(err.contains("h-key=\"todo.id\""));
|
||||||
|
assert!(err.contains("generated keyed helpers"));
|
||||||
|
assert!(err.contains(helper));
|
||||||
|
assert!(err.contains("Dynamic +data-key on the child"));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user