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:
Generated
+2
@@ -1409,6 +1409,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"hemplate",
|
||||||
|
"scraper",
|
||||||
"slhx",
|
"slhx",
|
||||||
"slhx-axum",
|
"slhx-axum",
|
||||||
"slhx-build",
|
"slhx-build",
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ path = "src/lib.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
hemplate = { path = "../../../hemplate/hemplate" }
|
||||||
slhx = { path = "../../slhx" }
|
slhx = { path = "../../slhx" }
|
||||||
slhx-axum = { path = "../../slhx-axum" }
|
slhx-axum = { path = "../../slhx-axum" }
|
||||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
scraper = "0.23"
|
||||||
slhx-test = { path = "../../slhx-test" }
|
slhx-test = { path = "../../slhx-test" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use axum::response::IntoResponse;
|
|||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use futures_util::{stream, StreamExt};
|
use futures_util::{stream, StreamExt};
|
||||||
|
use hemplate::Hemplate;
|
||||||
use slhx::{IntoEffect, SafeHtml};
|
use slhx::{IntoEffect, 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;
|
||||||
@@ -32,6 +33,12 @@ struct Card {
|
|||||||
column: usize,
|
column: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Hemplate)]
|
||||||
|
#[hemplate = "partials"]
|
||||||
|
struct Presence {
|
||||||
|
count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
@@ -80,13 +87,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.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());
|
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.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))
|
Some((Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT)), count + 1))
|
||||||
})
|
})
|
||||||
.boxed();
|
.boxed();
|
||||||
@@ -247,10 +254,13 @@ fn render_card(card: &Card) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_presence(count: u64) -> String {
|
fn render_presence(count: u64) -> SafeHtml {
|
||||||
format!(
|
// req: html_safety/002 req: view/001
|
||||||
r#"<span class="presence">Ada online</span> <span class="presence">Grace online</span> <small>tick #{count}</small>"#
|
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 {
|
fn escape_html(value: &str) -> String {
|
||||||
@@ -261,3 +271,28 @@ fn escape_html(value: &str) -> String {
|
|||||||
.replace('"', """)
|
.replace('"', """)
|
||||||
.replace('\'', "'")
|
.replace('\'', "'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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>
|
||||||
Reference in New Issue
Block a user