refactor(kanban): compose page with hemplate
Replace board page placeholder string replacement for options/board content with a hemplate page view and partial-rendered options. Cover page structure with DOM-aware scraper assertions. req: html_safety/002 req: view/001 req: test/005
This commit is contained in:
+57
-16
@@ -17,11 +17,11 @@ const COLUMNS: [(&str, &str); 3] = [("backlog", "Backlog"), ("doing", "Doing"),
|
||||
|
||||
#[derive(Default)]
|
||||
struct AppState {
|
||||
board: Mutex<Board>,
|
||||
board: Mutex<BoardState>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct Board {
|
||||
struct BoardState {
|
||||
next_id: u64,
|
||||
cards: Vec<Card>,
|
||||
}
|
||||
@@ -33,6 +33,25 @@ struct Card {
|
||||
column: usize,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
struct Board {
|
||||
options: SafeHtml,
|
||||
board: SafeHtml,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct ColumnOptions {
|
||||
options: Vec<ColumnOption>,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct ColumnOption {
|
||||
id: &'static str,
|
||||
title: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct Presence {
|
||||
@@ -42,7 +61,7 @@ struct Presence {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let state = Arc::new(AppState {
|
||||
board: Mutex::new(Board {
|
||||
board: Mutex::new(BoardState {
|
||||
next_id: 4,
|
||||
cards: vec![
|
||||
Card { id: 1, title: "Write requirements".into(), column: 0 },
|
||||
@@ -153,7 +172,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
||||
})
|
||||
}
|
||||
|
||||
fn board_effects(board: &Board, notice: &'static str) -> impl IntoEffect {
|
||||
fn board_effects(board: &BoardState, notice: &'static str) -> impl IntoEffect {
|
||||
(
|
||||
ui::board::slots::board.html(SafeHtml::trusted(render_board(board))),
|
||||
ui::board::slots::notice.text(notice),
|
||||
@@ -161,7 +180,7 @@ fn board_effects(board: &Board, notice: &'static str) -> impl IntoEffect {
|
||||
)
|
||||
}
|
||||
|
||||
fn update_card(board: &mut Board, card_id: Option<&str>, update: impl FnOnce(&mut Card)) -> bool {
|
||||
fn update_card(board: &mut BoardState, card_id: Option<&str>, update: impl FnOnce(&mut Card)) -> bool {
|
||||
let Some(id) = card_id.and_then(|id| id.parse::<u64>().ok()) else {
|
||||
return false;
|
||||
};
|
||||
@@ -180,10 +199,12 @@ fn parse_column(value: Option<&str>) -> usize {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn page_html(board: &Board) -> String {
|
||||
ui::board::lower_html(include_str!("../templates/board.heml"))
|
||||
.replace("__OPTIONS__", &render_options())
|
||||
.replace("__BOARD__", &render_board(board))
|
||||
fn page_html(board: &BoardState) -> String {
|
||||
// req: html_safety/002 req: view/001
|
||||
ui::board::lower_html(render_template(&Board {
|
||||
options: render_options(),
|
||||
board: SafeHtml::trusted(render_board(board)),
|
||||
}))
|
||||
}
|
||||
|
||||
fn shell(body: String) -> String {
|
||||
@@ -213,15 +234,17 @@ fn shell(body: String) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_options() -> String {
|
||||
COLUMNS
|
||||
fn render_options() -> SafeHtml {
|
||||
// req: html_safety/002 req: view/001
|
||||
render_html(&ColumnOptions {
|
||||
options: COLUMNS
|
||||
.iter()
|
||||
.map(|(id, title)| format!(r#"<option value="{}">{}</option>"#, escape_html(id), escape_html(title)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
.map(|(id, title)| ColumnOption { id, title })
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_board(board: &Board) -> String {
|
||||
fn render_board(board: &BoardState) -> String {
|
||||
let mut out = String::from("<div class=\"columns\">");
|
||||
for (idx, (_, title)) in COLUMNS.iter().enumerate() {
|
||||
out.push_str(&format!(r#"<section class="column"><h2>{}</h2>"#, escape_html(title)));
|
||||
@@ -260,7 +283,11 @@ fn render_presence(count: u64) -> SafeHtml {
|
||||
}
|
||||
|
||||
fn render_html(template: &impl Hemplate) -> SafeHtml {
|
||||
SafeHtml::trusted(template.render().expect("kanban hemplate partial renders"))
|
||||
SafeHtml::trusted(render_template(template))
|
||||
}
|
||||
|
||||
fn render_template(template: &impl Hemplate) -> String {
|
||||
template.render().expect("kanban hemplate view renders")
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
@@ -281,6 +308,20 @@ mod tests {
|
||||
Selector::parse(value).expect("test selector parses")
|
||||
}
|
||||
|
||||
// req: html_safety/002 req: view/001 req: test/005
|
||||
#[test]
|
||||
fn kanban_page_is_composed_by_a_hemplate_view() {
|
||||
let html = page_html(&BoardState::default());
|
||||
assert!(!html.contains("__OPTIONS__"));
|
||||
assert!(!html.contains("__BOARD__"));
|
||||
|
||||
let document = Html::parse_fragment(&html);
|
||||
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);
|
||||
}
|
||||
|
||||
// req: html_safety/002 req: view/001 req: test/005
|
||||
#[test]
|
||||
fn presence_payload_is_rendered_by_a_hemplate_view() {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<h1>slhx Kanban</h1>
|
||||
<form data-slhx-handle="create_card" data-slhx-form="create_card" data-slhx-disable-while-pending>
|
||||
<input name="title" type="text" required="required" placeholder="Card title">
|
||||
<select name="column" required="required">__OPTIONS__</select>
|
||||
<select name="column" required="required">{+= self.options =+}</select>
|
||||
<button type="submit">Add card</button>
|
||||
</form>
|
||||
<p data-slhx-slot="notice">Ready</p>
|
||||
</header>
|
||||
|
||||
<div data-slhx-slot="board">__BOARD__</div>
|
||||
<div data-slhx-slot="board">{+= self.board =+}</div>
|
||||
<aside data-slhx-slot="presence">Waiting for presence…</aside>
|
||||
|
||||
<button type="button" data-slhx-handle="move_left" hidden="hidden">Move left</button>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<option +value="self.id">{+ self.title +}</option>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template h-for="option in &self.options">
|
||||
{+ option +}
|
||||
</template>
|
||||
Reference in New Issue
Block a user