From b98820e819964105dcc4b8a003007a2db7f25e01 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 1 Jun 2026 20:48:18 +0200 Subject: [PATCH] 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 --- REQUIREMENTS.md | 2 +- slhx-build/src/lib.rs | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index c926d63..7329310 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -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. diff --git a/slhx-build/src/lib.rs b/slhx-build/src/lib.rs index c72537f..031bfe3 100644 --- a/slhx-build/src/lib.rs +++ b/slhx-build/src/lib.rs @@ -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#"
"#, + r#"
"#, ) .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#""#, + "data-slhx-on", + "click", + ), ] { let base = test_dir(&format!("slhx-build-invalid-convention-{case}-test")); let templates = base.join("templates");