feat(axum): add interaction registry alias

Add an interactions()/on() registration path and move canonical examples off explicit register_handle naming while keeping low-level registry types available underneath.

req: axum_integration/003

req: ceremony/001

req: dx/003

req: examples/003
This commit is contained in:
slhx agent
2026-06-02 00:24:32 +02:00
parent f388fa7d91
commit df4b4cc649
7 changed files with 41 additions and 25 deletions
+1 -1
View File
@@ -441,7 +441,7 @@ what a valid business email is.
002 Existing Axum routes remain normal Axum routes. slhx does not own routing. slhx-axum only mounts handler dispatch, runtime assets, and optional push endpoints.
### req: axum/003
003 Interactive fragments that would traditionally be implemented as `/demo/...` HTMX endpoints should be expressible as `#[slhx::handler]` functions returning generated slot commands.
003 Interactive fragments that would traditionally be implemented as `/demo/...` HTMX endpoints should be expressible as `#[slhx::handler]` functions returning generated slot commands. Integration registration should read as interactions over generated handles, not low-level registry wiring.
### req: axum/004
004 Query-string demo endpoints may be migrated to typed handler params from `data-*` attributes or forms. `Query<T>` remains available in normal Axum routes but is not the slhx happy path.
+6 -6
View File
@@ -6,7 +6,7 @@ use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{IntoEffect, RenderSlotExt, SafeHtml};
use slhx_axum::{
handlers, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
interactions, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
InteractionRequest, PageRequest,
};
use slhx_kanban_example::ui::{self, board as board_ui};
@@ -152,8 +152,8 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
}
fn registry(state: Arc<AppState>) -> impl DispatchRegistry {
handlers(ui::BUILD_FINGERPRINT)
.register_handle(handles::create_card, {
interactions(ui::BUILD_FINGERPRINT)
.on(handles::create_card, {
let state = state.clone();
move |form| {
// req: examples/001 req: form/002
@@ -168,7 +168,7 @@ fn registry(state: Arc<AppState>) -> impl DispatchRegistry {
board_effects(&board, "Card added")
}
})
.register_handle(card_handles::move_left, {
.on(card_handles::move_left, {
let state = state.clone();
move |form| {
// req: examples/001 req: list/003
@@ -179,7 +179,7 @@ fn registry(state: Arc<AppState>) -> impl DispatchRegistry {
board_effects(&board, if moved { "Card moved left" } else { "Card not found" })
}
})
.register_handle(card_handles::move_right, {
.on(card_handles::move_right, {
let state = state.clone();
move |form| {
// req: examples/001 req: list/003
@@ -190,7 +190,7 @@ fn registry(state: Arc<AppState>) -> impl DispatchRegistry {
board_effects(&board, if moved { "Card moved right" } else { "Card not found" })
}
})
.register_handle(card_handles::delete_card, {
.on(card_handles::delete_card, {
let state = state.clone();
move |form| {
// req: examples/001 req: list/003
+9 -9
View File
@@ -6,7 +6,7 @@ use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{CssClass, CssClasses, IntoEffect, RenderSlotExt, SafeHtml};
use slhx_axum::{
handlers, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
interactions, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
InteractionRequest, PageRequest,
};
use slhx_techdemo::ui;
@@ -283,8 +283,8 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
}
fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
handlers(ui::BUILD_FINGERPRINT)
.register_handle(handles::launch_work, {
interactions(ui::BUILD_FINGERPRINT)
.on(handles::launch_work, {
let shared = shared.clone();
move |form| {
// req: form/002 req: examples/001
@@ -303,7 +303,7 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
demo_effects(&demo, "Launch accepted · 4 targets updated")
}
})
.register_handle(card_handles::advance_work, {
.on(card_handles::advance_work, {
let shared = shared.clone();
move |form| {
// req: list/003 req: examples/001
@@ -321,7 +321,7 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
demo_effects(&demo, "Pipeline advanced")
}
})
.register_handle(lane_handles::move_to_lane, {
.on(lane_handles::move_to_lane, {
let shared = shared.clone();
move |form| {
// req: list/003 req: examples/001
@@ -343,7 +343,7 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
demo_effects(&demo, "Drag-and-drop move persisted")
}
})
.register_handle(card_handles::delete_work, {
.on(card_handles::delete_work, {
let shared = shared.clone();
move |form| {
// req: list/003 req: examples/001
@@ -362,7 +362,7 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
demo_effects(&demo, "Card removed")
}
})
.register_handle(card_handles::spotlight_work, {
.on(card_handles::spotlight_work, {
let shared = shared.clone();
move |form| {
// req: examples/001
@@ -377,7 +377,7 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
demo_effects(&demo, "Inspector focused")
}
})
.register_handle(handles::simulate_push, {
.on(handles::simulate_push, {
let shared = shared.clone();
move |_| {
// req: push/003 req: examples/001
@@ -393,7 +393,7 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
)
}
})
.register_handle(handles::reset_demo, {
.on(handles::reset_demo, {
let shared = shared.clone();
move |_| {
// req: examples/001
+7 -7
View File
@@ -6,7 +6,7 @@ use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{push, IntoEffect, RenderSlotExt, SafeHtml};
use slhx_axum::{
handlers, runtime_js, sse, DispatchRegistry, EffectResponse, InteractionRequest, PageRequest,
interactions, runtime_js, sse, DispatchRegistry, EffectResponse, InteractionRequest, PageRequest,
};
use slhx_v0_examples::ui;
use slhx_v0_examples::ui::{auth, counter, notifications, page_swap, todos, wizard};
@@ -124,8 +124,8 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
}
fn registry(state: Arc<ExampleState>) -> impl DispatchRegistry {
handlers(ui::BUILD_FINGERPRINT)
.register_handle(counter_handles::increment, {
interactions(ui::BUILD_FINGERPRINT)
.on(counter_handles::increment, {
let state = state.clone();
move |_| {
// req: examples/001
@@ -134,7 +134,7 @@ fn registry(state: Arc<ExampleState>) -> impl DispatchRegistry {
counter_slots::counter_value.text(*counter)
}
})
.register_handle(todo_handles::add_todo, {
.on(todo_handles::add_todo, {
let state = state.clone();
move |form| {
// req: examples/001
@@ -150,7 +150,7 @@ fn registry(state: Arc<ExampleState>) -> impl DispatchRegistry {
)
}
})
.register_handle(wizard_handles::next_step, {
.on(wizard_handles::next_step, {
let state = state.clone();
move |_| {
// req: examples/001
@@ -159,7 +159,7 @@ fn registry(state: Arc<ExampleState>) -> impl DispatchRegistry {
wizard_slots::wizard_step.text(format!("Step {}", *step + 1))
}
})
.register_handle(auth_handles::login, |form| {
.on(auth_handles::login, |form| {
// req: examples/001
let ok = form.value("email") == Some("demo@example.com")
&& form.value("password").is_some_and(|password| !password.is_empty());
@@ -169,7 +169,7 @@ fn registry(state: Arc<ExampleState>) -> impl DispatchRegistry {
"Try demo@example.com with any password"
})
})
.register_handle(page_handles::load_docs, |_| {
.on(page_handles::load_docs, |_| {
// req: page_swap/002, req: examples/001
(
page_slots::content.render(&DocsContent {
+15
View File
@@ -266,6 +266,10 @@ pub const fn handlers(fingerprint: BuildFingerprint) -> HandlerRegistry {
HandlerRegistry::new(fingerprint)
}
pub const fn interactions(fingerprint: BuildFingerprint) -> HandlerRegistry {
HandlerRegistry::new(fingerprint)
}
impl InteractionRequest {
pub fn dispatch(
self,
@@ -320,6 +324,17 @@ impl HandlerRegistry {
self.register(handle.id().id, handler)
}
pub fn on<I, E>(
self,
handle: Handle<I>,
handler: impl Fn(InteractionForm) -> E + Send + Sync + 'static,
) -> Self
where
E: IntoEffect,
{
self.register_handle(handle, handler)
}
pub fn dispatch(&self, form: InteractionForm) -> Result<EffectResponse, DispatchRejection> {
let handle_id = form.handle_id;
let Some(handler) = self.handlers.get(&handle_id) else {
+2 -2
View File
@@ -2,7 +2,7 @@ use axum::http::{header, HeaderMap};
use axum::response::IntoResponse;
use scraper::{Html, Selector};
use slhx_axum::{
handlers, runtime_js, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm,
interactions, runtime_js, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm,
InteractionFormRejection, InteractionRequest, PageMode, PageRequest, PageResponse,
SLHX_CONTENT_TYPE, SLHX_FINGERPRINT_HEADER, SLHX_PARTIAL_HEADER, SLHX_RUNTIME_CONTENT_TYPE,
SLHX_TITLE_HEADER,
@@ -148,7 +148,7 @@ fn interaction_request_dispatches_with_concise_handlers_helper() {
Vec::new(),
));
let response = request
.dispatch(handlers(BuildFingerprint(4)).register_handle(Handle::<()>::new(7), |_| {
.dispatch(interactions(BuildFingerprint(4)).on(Handle::<()>::new(7), |_| {
Slot::<String>::new(3).text("ok")
}))
.unwrap();
+1
View File
@@ -65,6 +65,7 @@ fn canonical_examples_do_not_author_low_level_resource_plumbing() {
"render_html(",
"HandlerRegistry",
"InteractionForm",
"register_handle(",
"name=\"__h\"",
"name='__h'",
];