refactor(kanban): render presence with hemplate

Move the kanban SSE presence fragment from Rust raw HTML formatting into a hemplate partial/view and cover the rendered structure with scraper.

req: html_safety/002

req: view/001

req: test/005
This commit is contained in:
slhx agent
2026-05-25 21:34:53 +02:00
parent 59b0c8ccc4
commit d76682e98f
4 changed files with 46 additions and 6 deletions
+2
View File
@@ -10,11 +10,13 @@ path = "src/lib.rs"
[dependencies]
axum = "0.7"
futures-util = "0.3"
hemplate = { path = "../../../hemplate/hemplate" }
slhx = { path = "../../slhx" }
slhx-axum = { path = "../../slhx-axum" }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
[dev-dependencies]
scraper = "0.23"
slhx-test = { path = "../../slhx-test" }
[build-dependencies]
+41 -6
View File
@@ -3,6 +3,7 @@ use axum::response::IntoResponse;
use axum::routing::get;
use axum::Router;
use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{IntoEffect, SafeHtml};
use slhx_axum::{runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
use slhx_kanban_example::ui;
@@ -32,6 +33,12 @@ struct Card {
column: usize,
}
#[derive(Hemplate)]
#[hemplate = "partials"]
struct Presence {
count: u64,
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
@@ -80,13 +87,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.html(SafeHtml::trusted(render_presence(1)));
let effect = ui::board::slots::presence.html(render_presence(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.html(SafeHtml::trusted(render_presence(count)));
let effect = ui::board::slots::presence.html(render_presence(count));
Some((Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT)), count + 1))
})
.boxed();
@@ -247,10 +254,13 @@ fn render_card(card: &Card) -> String {
)
}
fn render_presence(count: u64) -> String {
format!(
r#"<span class="presence">Ada online</span> <span class="presence">Grace online</span> <small>tick #{count}</small>"#
)
fn render_presence(count: u64) -> SafeHtml {
// req: html_safety/002 req: view/001
render_html(&Presence { count })
}
fn render_html(template: &impl Hemplate) -> SafeHtml {
SafeHtml::trusted(template.render().expect("kanban hemplate partial renders"))
}
fn escape_html(value: &str) -> String {
@@ -261,3 +271,28 @@ fn escape_html(value: &str) -> String {
.replace('"', "&quot;")
.replace('\'', "&#39;")
}
#[cfg(test)]
mod tests {
use super::*;
use scraper::{Html, Selector};
fn selector(value: &str) -> Selector {
Selector::parse(value).expect("test selector parses")
}
// req: html_safety/002 req: view/001 req: test/005
#[test]
fn presence_payload_is_rendered_by_a_hemplate_view() {
let html = render_presence(7);
let document = Html::parse_fragment(html.as_str());
assert_eq!(document.select(&selector("span.presence")).count(), 2);
assert_eq!(
document
.select(&selector("small"))
.next()
.map(|small| small.text().collect::<String>()),
Some("tick #7".to_owned())
);
}
}
@@ -0,0 +1 @@
<span class="presence">Ada online</span> <span class="presence">Grace online</span> <small>tick #{+ self.count +}</small>