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
+1 -1
View File
@@ -69,7 +69,7 @@ mod tests {
// req: examples/001 req: form/002 req: codegen/003 // req: examples/001 req: form/002 req: codegen/003
#[test] #[test]
fn kanban_template_exports_form_and_card_handles() { fn kanban_template_exports_form_and_card_handles() {
assert_ne!(ui::board::handles::create_card.id(), ui::board::handles::move_right.id()); assert_ne!(ui::board::handles::create_card.id(), ui::board_card::handles::move_right.id());
assert_eq!(ui::board::forms::create_card.field("title").resource, ui::board::forms::create_card.id()); assert_eq!(ui::board::forms::create_card.field("title").resource, ui::board::forms::create_card.id());
} }
} }
+18 -29
View File
@@ -4,9 +4,11 @@ use axum::routing::get;
use axum::Router; use axum::Router;
use futures_util::{stream, StreamExt}; use futures_util::{stream, StreamExt};
use hemplate::Hemplate; 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_axum::{runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
use slhx_kanban_example::ui; 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::collections::BTreeMap;
use std::convert::Infallible; use std::convert::Infallible;
use std::net::SocketAddr; use std::net::SocketAddr;
@@ -51,9 +53,6 @@ struct BoardColumn {
struct BoardCard { struct BoardCard {
id: u64, id: u64,
title: String, title: String,
left_handle: Handle<()>,
right_handle: Handle<()>,
delete_handle: Handle<()>,
left_disabled: bool, left_disabled: bool,
right_disabled: bool, right_disabled: bool,
} }
@@ -131,13 +130,13 @@ async fn interact(
// req: push/001 req: push/003 req: examples/001 // req: push/001 req: push/003 req: examples/001
async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResponse { async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResponse {
if params.contains_key("once") { 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()); return sse(stream::iter([Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT))]).boxed());
} }
let batches = stream::unfold(1_u64, |count| async move { let batches = stream::unfold(1_u64, |count| async move {
tokio::time::sleep(Duration::from_secs(4)).await; 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)) Some((Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT)), count + 1))
}) })
.boxed(); .boxed();
@@ -146,7 +145,7 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
fn registry(state: Arc<AppState>) -> HandlerRegistry { fn registry(state: Arc<AppState>) -> HandlerRegistry {
HandlerRegistry::new(ui::BUILD_FINGERPRINT) HandlerRegistry::new(ui::BUILD_FINGERPRINT)
.register_handle(ui::board::handles::create_card, { .register_handle(handles::create_card, {
let state = state.clone(); let state = state.clone();
move |form| { move |form| {
// req: examples/001 req: form/002 // req: examples/001 req: form/002
@@ -161,7 +160,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
board_effects(&board, "Card added") board_effects(&board, "Card added")
} }
}) })
.register_handle(ui::board::handles::move_left, { .register_handle(card_handles::move_left, {
let state = state.clone(); let state = state.clone();
move |form| { move |form| {
// req: examples/001 req: list/003 // 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" }) 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(); let state = state.clone();
move |form| { move |form| {
// req: examples/001 req: list/003 // 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" }) 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(); let state = state.clone();
move |form| { move |form| {
// req: examples/001 req: list/003 // 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 { fn board_effects(board: &BoardState, notice: &'static str) -> impl IntoEffect {
( (
ui::board::slots::board.render(&board_view(board)), slots::board.html(render_board(board)),
ui::board::slots::notice.text(notice), slots::notice.text(notice),
ui::board::forms::create_card.clear("title"), forms::create_card.clear("title"),
) )
} }
@@ -227,10 +226,11 @@ fn parse_column(value: Option<&str>) -> usize {
fn page_html(board: &BoardState) -> String { fn page_html(board: &BoardState) -> String {
// Explicit full-page composition boundary for already-rendered hemplate fragments. // Explicit full-page composition boundary for already-rendered hemplate fragments.
// req: html_safety/002 req: view/001 // req: html_safety/002 req: view/001
ui::board::lower_html(render_template(&Board { ui::board::render_html(&Board {
options: render_options(), options: render_options(),
board: render_board(board), board: render_board(board),
})) })
.into_string()
} }
fn shell(body: String) -> String { fn shell(body: String) -> String {
@@ -262,7 +262,7 @@ fn shell(body: String) -> String {
fn render_options() -> SafeHtml { fn render_options() -> SafeHtml {
// req: html_safety/002 req: view/001 // req: html_safety/002 req: view/001
slhx::render_html(&ColumnOptions { ui::render_html(&ColumnOptions {
options: COLUMNS options: COLUMNS
.iter() .iter()
.map(|(id, title)| ColumnOption { id, title }) .map(|(id, title)| ColumnOption { id, title })
@@ -291,29 +291,18 @@ fn board_view(board: &BoardState) -> BoardColumns {
fn render_board(board: &BoardState) -> SafeHtml { fn render_board(board: &BoardState) -> SafeHtml {
// req: html_safety/002 req: view/001 // 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 { fn render_card(card: &Card) -> BoardCard {
BoardCard { BoardCard {
id: card.id, id: card.id,
title: card.title.clone(), 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, left_disabled: card.column == 0,
right_disabled: card.column + 1 == COLUMNS.len(), 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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("section[data-slhx-root=\"kanban\"]")).count(), 1);
assert_eq!(document.select(&selector("select[name=\"column\"] > option")).count(), 3); 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-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 // req: html_safety/002 req: view/001 req: test/005
-3
View File
@@ -12,7 +12,4 @@
<div data-slhx-slot="board">{+= self.board =+}</div> <div data-slhx-slot="board">{+= self.board =+}</div>
<aside data-slhx-slot="presence">Waiting for presence…</aside> <aside data-slhx-slot="presence">Waiting for presence…</aside>
<button type="button" data-slhx-handle="move_left" hidden="hidden">Move left</button>
<button type="button" data-slhx-handle="move_right" hidden="hidden">Move right</button>
<button type="button" data-slhx-handle="delete_card" hidden="hidden">Delete</button>
</section> </section>
@@ -1,10 +1,10 @@
<article class="card" +data-key="self.id"> <article class="card" +data-key="self.id">
<strong>{+ self.title +}</strong> <strong>{+ self.title +}</strong>
<menu> <menu>
<button h-if="self.left_disabled" type="button" +data-hid="self.left_handle" +data-card-id="self.id" disabled="disabled">←</button> <button h-if="self.left_disabled" type="button" data-slhx-handle="move_left" +data-card-id="self.id" disabled="disabled">←</button>
<button h-else type="button" +data-hid="self.left_handle" +data-card-id="self.id">←</button> <button h-else type="button" data-slhx-handle="move_left" +data-card-id="self.id">←</button>
<button h-if="self.right_disabled" type="button" +data-hid="self.right_handle" +data-card-id="self.id" disabled="disabled">→</button> <button h-if="self.right_disabled" type="button" data-slhx-handle="move_right" +data-card-id="self.id" disabled="disabled">→</button>
<button h-else type="button" +data-hid="self.right_handle" +data-card-id="self.id">→</button> <button h-else type="button" data-slhx-handle="move_right" +data-card-id="self.id">→</button>
<button type="button" +data-hid="self.delete_handle" +data-card-id="self.id">Delete</button> <button type="button" data-slhx-handle="delete_card" +data-card-id="self.id">Delete</button>
</menu> </menu>
</article> </article>