fix(build): reject unsupported slhx events

Validate static data-slhx-on values against the runtime-supported delegated event set so generated EventName constants cannot imply browser behavior the runtime will never dispatch.

req: convention/002

req: diagnostics/002
This commit is contained in:
slhx agent
2026-06-01 20:48:18 +02:00
parent 44366252f7
commit b98820e819
2 changed files with 22 additions and 4 deletions
+1 -1
View File
@@ -864,7 +864,7 @@ All forms support returning `impl IntoEffect` and compose through tuples.
001 slhx-axum and the JS runtime support common UX conventions as attributes, not core effects: `data-slhx-pending-class`, `data-slhx-indicator`, `data-slhx-confirm`, `data-slhx-debounce`, `data-slhx-throttle`, `data-slhx-every`, `data-slhx-disable-while-pending`, `data-slhx-policy`, and `data-slhx-on`. These are orthogonal to the core effect algebra.
### req: convention/002
002 Default event triggers: `submit` for forms, `click` for buttons and links. `data-slhx-on` overrides the default.
002 Default event triggers: `submit` for forms, `click` for buttons and links. `data-slhx-on` overrides the default for the runtime-supported delegated events: `click`, `submit`, `input`, `change`, `dragstart`, `dragover`, and `drop`. Unsupported static event names are build errors.
### req: convention/003
003 `data-slhx-debounce` and `data-slhx-throttle` support simple millisecond values. No trigger mini-language in core.
+21 -3
View File
@@ -1027,6 +1027,14 @@ fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
"expected one of `latest`, `queue`, `drop`, or `parallel`",
));
}
"data-slhx-on" if !event_tokens(value).all(valid_runtime_event) => {
return Err(invalid_slhx_value(
path,
&attr.name,
value,
"expected runtime-supported events: `click`, `submit`, `input`, `change`, `dragstart`, `dragover`, or `drop`",
));
}
"data-slhx-debounce" | "data-slhx-throttle" | "data-slhx-every"
if !valid_duration(value) =>
{
@@ -1047,6 +1055,10 @@ fn valid_policy(value: &str) -> bool {
matches!(value.trim(), "latest" | "queue" | "drop" | "parallel")
}
fn valid_runtime_event(value: &str) -> bool {
matches!(value, "click" | "submit" | "input" | "change" | "dragstart" | "dragover" | "drop")
}
fn valid_duration(value: &str) -> bool {
let value = value.trim();
let digits = value
@@ -1195,7 +1207,7 @@ mod tests {
std::fs::create_dir_all(&templates).unwrap();
std::fs::write(
templates.join("todo.heml"),
r#"<form data-slhx-handle="create" data-slhx-form="new_todo"><input name="title"></form><button data-slhx-handle="delete" data-todo-id="7" data-slhx-on="click keydown">Delete</button><ul data-slhx-slot="todos"></ul><section data-slhx-atom="filter"></section>"#,
r#"<form data-slhx-handle="create" data-slhx-form="new_todo"><input name="title"></form><button data-slhx-handle="delete" data-todo-id="7" data-slhx-on="click change">Delete</button><ul data-slhx-slot="todos"></ul><section data-slhx-atom="filter"></section>"#,
)
.unwrap();
@@ -1215,7 +1227,7 @@ mod tests {
assert!(generated.contains("pub const filter"));
assert!(generated.contains("pub mod events"));
assert!(generated.contains("pub const click: ::slhx::EventName = ::slhx::EventName::new(\"click\")"));
assert!(generated.contains("pub const keydown: ::slhx::EventName = ::slhx::EventName::new(\"keydown\")"));
assert!(generated.contains("pub const change: ::slhx::EventName = ::slhx::EventName::new(\"change\")"));
assert!(generated.contains("pub const new_todo"));
assert!(generated.contains("pub mod todo"));
assert!(generated.contains("pub const ALL_IDS"));
@@ -1233,7 +1245,7 @@ mod tests {
assert!(syms.contains("handle_param\tdelete\ttodo_id\n"));
assert!(syms.contains("event\t"));
assert!(syms.contains("\tclick\tclick\n"));
assert!(syms.contains("\tkeydown\tkeydown\n"));
assert!(syms.contains("\tchange\tchange\n"));
let _ = std::fs::remove_dir_all(&base);
}
@@ -1537,6 +1549,12 @@ fn main() {{
"data-slhx-every",
"1s",
),
(
"event",
r#"<button data-slhx-handle="save" data-slhx-on="keydown">Save</button>"#,
"data-slhx-on",
"click",
),
] {
let base = test_dir(&format!("slhx-build-invalid-convention-{case}-test"));
let templates = base.join("templates");