feat(slhx): render hemplate views into slots

Add render_html plus slot render_view helpers that call hemplate render_into, and migrate a kanban effect path from explicit SafeHtml/html handling to the renderable-view helper.

req: view/001

req: html_safety/002
This commit is contained in:
slhx agent
2026-05-26 00:53:43 +02:00
parent 62316d5f1e
commit b6ecb9513c
4 changed files with 56 additions and 13 deletions
Generated
+1
View File
@@ -1360,6 +1360,7 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
name = "slhx" name = "slhx"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"hemplate",
"slhx-core", "slhx-core",
"slhx-derive", "slhx-derive",
] ]
+13 -13
View File
@@ -4,7 +4,7 @@ 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, SafeHtml}; use slhx::{Handle, 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 std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -131,13 +131,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(render_presence(1)); let effect = ui::board::slots::presence.render_view(&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.html(render_presence(count)); let effect = ui::board::slots::presence.render_view(&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();
@@ -199,7 +199,7 @@ 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.html(render_board(board)), ui::board::slots::board.render_view(&board_view(board)),
ui::board::slots::notice.text(notice), ui::board::slots::notice.text(notice),
ui::board::forms::create_card.clear("title"), ui::board::forms::create_card.clear("title"),
) )
@@ -269,9 +269,9 @@ fn render_options() -> SafeHtml {
}) })
} }
fn render_board(board: &BoardState) -> SafeHtml { fn board_view(board: &BoardState) -> BoardColumns {
// req: html_safety/002 req: view/001 // req: html_safety/002 req: view/001
render_html(&BoardColumns { BoardColumns {
columns: COLUMNS columns: COLUMNS
.iter() .iter()
.enumerate() .enumerate()
@@ -285,7 +285,12 @@ fn render_board(board: &BoardState) -> SafeHtml {
.collect(), .collect(),
}) })
.collect(), .collect(),
}) }
}
fn render_board(board: &BoardState) -> SafeHtml {
// req: html_safety/002 req: view/001
render_html(&board_view(board))
} }
fn render_card(card: &Card) -> BoardCard { fn render_card(card: &Card) -> BoardCard {
@@ -300,11 +305,6 @@ fn render_card(card: &Card) -> BoardCard {
} }
} }
fn render_presence(count: u64) -> SafeHtml {
// req: html_safety/002 req: view/001
render_html(&Presence { count })
}
fn render_html(template: &impl Hemplate) -> SafeHtml { fn render_html(template: &impl Hemplate) -> SafeHtml {
SafeHtml::trusted(render_template(template)) SafeHtml::trusted(render_template(template))
} }
@@ -368,7 +368,7 @@ mod tests {
// req: html_safety/002 req: view/001 req: test/005 // req: html_safety/002 req: view/001 req: test/005
#[test] #[test]
fn presence_payload_is_rendered_by_a_hemplate_view() { fn presence_payload_is_rendered_by_a_hemplate_view() {
let html = render_presence(7); let html = render_html(&Presence { count: 7 });
let document = Html::parse_fragment(html.as_str()); let document = Html::parse_fragment(html.as_str());
assert_eq!(document.select(&selector("span.presence")).count(), 2); assert_eq!(document.select(&selector("span.presence")).count(), 2);
assert_eq!( assert_eq!(
+1
View File
@@ -7,5 +7,6 @@ edition.workspace = true
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
hemplate = { path = "../../hemplate/hemplate" }
slhx-core = { path = "../slhx-core" } slhx-core = { path = "../slhx-core" }
slhx-derive = { path = "../slhx-derive" } slhx-derive = { path = "../slhx-derive" }
+41
View File
@@ -6,11 +6,52 @@
pub use slhx_core::*; pub use slhx_core::*;
pub use slhx_derive::{app, component, form, handler, surface}; pub use slhx_derive::{app, component, form, handler, surface};
pub fn render_html(view: &impl hemplate::Hemplate) -> SafeHtml {
let mut html = String::new();
view.render_into(&mut html)
.expect("hemplate view renders into slhx effect payload");
SafeHtml::trusted(html)
}
pub trait RenderSlotExt {
fn render_view(self, view: &impl hemplate::Hemplate) -> Effect;
}
impl<T> RenderSlotExt for Slot<T> {
fn render_view(self, view: &impl hemplate::Hemplate) -> Effect {
self.html(render_html(view))
}
}
pub trait RenderKeyedSlotExt<K> {
fn append_view(self, key: K, view: &impl hemplate::Hemplate) -> Effect;
fn prepend_view(self, key: K, view: &impl hemplate::Hemplate) -> Effect;
fn replace_view(self, key: K, view: &impl hemplate::Hemplate) -> Effect;
}
impl<K, T> RenderKeyedSlotExt<K> for KeyedSlot<K, T>
where
K: ToString,
{
fn append_view(self, key: K, view: &impl hemplate::Hemplate) -> Effect {
self.append_html(key, render_html(view))
}
fn prepend_view(self, key: K, view: &impl hemplate::Hemplate) -> Effect {
self.prepend_html(key, render_html(view))
}
fn replace_view(self, key: K, view: &impl hemplate::Hemplate) -> Effect {
self.replace_html(key, render_html(view))
}
}
pub mod prelude { pub mod prelude {
pub use slhx_core::{ pub use slhx_core::{
navigate, push, redirect, replace, Atom, BuildFingerprint, ComponentRef, CssClass, navigate, push, redirect, replace, Atom, BuildFingerprint, ComponentRef, CssClass,
CssClasses, Effect, EventName, Form, FormModel, FormValue, Handle, IntoEffect, KeyedSlot, CssClasses, Effect, EventName, Form, FormModel, FormValue, Handle, IntoEffect, KeyedSlot,
Slot, Slot,
}; };
pub use crate::{render_html, RenderKeyedSlotExt, RenderSlotExt};
pub use slhx_derive::{app, component, form, handler, surface}; pub use slhx_derive::{app, component, form, handler, surface};
} }