refactor(kanban): scope card handles to partials

Move kanban card actions onto the reusable card partial, use generated render helpers for board/page composition, and remove manual handle fields plus hidden shell-level action anchors from the composed path.

req: component/003

req: view/001

req: examples/001
This commit is contained in:
slhx agent
2026-06-01 22:25:23 +02:00
parent 4d79dd4dba
commit 54dd401e8b
4 changed files with 24 additions and 38 deletions
+18 -29
View File
@@ -4,9 +4,11 @@ use axum::routing::get;
use axum::Router;
use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{Handle, IntoEffect, RenderSlotExt, SafeHtml};
use slhx::{IntoEffect, RenderSlotExt, SafeHtml};
use slhx_axum::{runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
use slhx_kanban_example::ui;
use slhx_kanban_example::ui::board::{forms, handles, slots};
use slhx_kanban_example::ui::board_card::handles as card_handles;
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::net::SocketAddr;
@@ -51,9 +53,6 @@ struct BoardColumn {
struct BoardCard {
id: u64,
title: String,
left_handle: Handle<()>,
right_handle: Handle<()>,
delete_handle: Handle<()>,
left_disabled: bool,
right_disabled: bool,
}
@@ -131,13 +130,13 @@ async fn interact(
// req: push/001 req: push/003 req: examples/001
async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResponse {
if params.contains_key("once") {
let effect = ui::board::slots::presence.render(&Presence { count: 1 });
let effect = slots::presence.render(&Presence { count: 1 });
return sse(stream::iter([Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT))]).boxed());
}
let batches = stream::unfold(1_u64, |count| async move {
tokio::time::sleep(Duration::from_secs(4)).await;
let effect = ui::board::slots::presence.render(&Presence { count });
let effect = slots::presence.render(&Presence { count });
Some((Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT)), count + 1))
})
.boxed();
@@ -146,7 +145,7 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
fn registry(state: Arc<AppState>) -> HandlerRegistry {
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
.register_handle(ui::board::handles::create_card, {
.register_handle(handles::create_card, {
let state = state.clone();
move |form| {
// req: examples/001 req: form/002
@@ -161,7 +160,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
board_effects(&board, "Card added")
}
})
.register_handle(ui::board::handles::move_left, {
.register_handle(card_handles::move_left, {
let state = state.clone();
move |form| {
// req: examples/001 req: list/003
@@ -172,7 +171,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
board_effects(&board, if moved { "Card moved left" } else { "Card not found" })
}
})
.register_handle(ui::board::handles::move_right, {
.register_handle(card_handles::move_right, {
let state = state.clone();
move |form| {
// req: examples/001 req: list/003
@@ -183,7 +182,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
board_effects(&board, if moved { "Card moved right" } else { "Card not found" })
}
})
.register_handle(ui::board::handles::delete_card, {
.register_handle(card_handles::delete_card, {
let state = state.clone();
move |form| {
// req: examples/001 req: list/003
@@ -199,9 +198,9 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
fn board_effects(board: &BoardState, notice: &'static str) -> impl IntoEffect {
(
ui::board::slots::board.render(&board_view(board)),
ui::board::slots::notice.text(notice),
ui::board::forms::create_card.clear("title"),
slots::board.html(render_board(board)),
slots::notice.text(notice),
forms::create_card.clear("title"),
)
}
@@ -227,10 +226,11 @@ fn parse_column(value: Option<&str>) -> usize {
fn page_html(board: &BoardState) -> String {
// Explicit full-page composition boundary for already-rendered hemplate fragments.
// req: html_safety/002 req: view/001
ui::board::lower_html(render_template(&Board {
ui::board::render_html(&Board {
options: render_options(),
board: render_board(board),
}))
})
.into_string()
}
fn shell(body: String) -> String {
@@ -262,7 +262,7 @@ fn shell(body: String) -> String {
fn render_options() -> SafeHtml {
// req: html_safety/002 req: view/001
slhx::render_html(&ColumnOptions {
ui::render_html(&ColumnOptions {
options: COLUMNS
.iter()
.map(|(id, title)| ColumnOption { id, title })
@@ -291,29 +291,18 @@ fn board_view(board: &BoardState) -> BoardColumns {
fn render_board(board: &BoardState) -> SafeHtml {
// req: html_safety/002 req: view/001
slhx::render_html(&board_view(board))
ui::render_html(&board_view(board))
}
fn render_card(card: &Card) -> BoardCard {
BoardCard {
id: card.id,
title: card.title.clone(),
left_handle: ui::board::handles::move_left,
right_handle: ui::board::handles::move_right,
delete_handle: ui::board::handles::delete_card,
left_disabled: card.column == 0,
right_disabled: card.column + 1 == COLUMNS.len(),
}
}
fn render_template(template: &impl Hemplate) -> String {
let mut html = String::new();
template
.render_into(&mut html)
.expect("kanban hemplate view renders");
html
}
#[cfg(test)]
mod tests {
use super::*;
@@ -334,7 +323,7 @@ mod tests {
assert_eq!(document.select(&selector("section[data-slhx-root=\"kanban\"]")).count(), 1);
assert_eq!(document.select(&selector("select[name=\"column\"] > option")).count(), 3);
assert_eq!(document.select(&selector("[data-sid]")).count(), 3);
assert_eq!(document.select(&selector("[data-hid]")).count(), 4);
assert_eq!(document.select(&selector("[data-hid]")).count(), 1);
}
// req: html_safety/002 req: view/001 req: test/005