refactor(v0): render todos with hemplate
Move the v0 todo-list payload from Rust raw HTML formatting into a hemplate partial/view and cover both populated and empty rendered structure with scraper. req: html_safety/002 req: view/001 req: test/005
This commit is contained in:
Generated
+2
@@ -1447,6 +1447,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]
|
||||||
|
|||||||
+59
-16
@@ -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::{push, IntoEffect, SafeHtml};
|
use slhx::{push, IntoEffect, SafeHtml};
|
||||||
use slhx_axum::{runtime_js, sse, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
|
use slhx_axum::{runtime_js, sse, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
|
||||||
use slhx_v0_examples::ui;
|
use slhx_v0_examples::ui;
|
||||||
@@ -25,6 +26,17 @@ struct Todo {
|
|||||||
title: String,
|
title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Hemplate)]
|
||||||
|
#[hemplate = "partials"]
|
||||||
|
struct TodoItems {
|
||||||
|
items: Vec<TodoItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TodoItem {
|
||||||
|
id: u64,
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let state = Arc::new(ExampleState::default());
|
let state = Arc::new(ExampleState::default());
|
||||||
@@ -116,7 +128,7 @@ fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
|||||||
todos.push(Todo { id, title: title.into() });
|
todos.push(Todo { id, title: title.into() });
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
ui::todos::slots::todo_list.html(SafeHtml::trusted(render_todos(&todos))),
|
ui::todos::slots::todo_list.html(render_todos(&todos)),
|
||||||
ui::todos::forms::new_todo.clear("title"),
|
ui::todos::forms::new_todo.clear("title"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -189,22 +201,53 @@ fn shell(body: String) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_todos(todos: &[Todo]) -> String {
|
fn render_todos(todos: &[Todo]) -> SafeHtml {
|
||||||
if todos.is_empty() {
|
// req: html_safety/002 req: view/001
|
||||||
return "<li>No todos yet</li>".into();
|
render_html(&TodoItems {
|
||||||
}
|
items: todos
|
||||||
todos
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|todo| format!(r#"<li data-key="{}">{}</li>"#, todo.id, escape_html(&todo.title)))
|
.map(|todo| TodoItem {
|
||||||
.collect::<Vec<_>>()
|
id: todo.id,
|
||||||
.join("")
|
title: todo.title.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_html(value: &str) -> String {
|
fn render_html(template: &impl Hemplate) -> SafeHtml {
|
||||||
value
|
SafeHtml::trusted(template.render().expect("v0 hemplate partial renders"))
|
||||||
.replace('&', "&")
|
}
|
||||||
.replace('<', "<")
|
|
||||||
.replace('>', ">")
|
#[cfg(test)]
|
||||||
.replace('"', """)
|
mod tests {
|
||||||
.replace('\'', "'")
|
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 todos_payload_is_rendered_by_a_hemplate_view() {
|
||||||
|
let todos = vec![Todo { id: 7, title: "<b>Ship v0</b>".to_owned() }];
|
||||||
|
|
||||||
|
let html = render_todos(&todos);
|
||||||
|
let document = Html::parse_fragment(html.as_str());
|
||||||
|
let rows = document.select(&selector("li[data-key]")).collect::<Vec<_>>();
|
||||||
|
assert_eq!(rows.len(), 1);
|
||||||
|
assert_eq!(rows[0].value().attr("data-key"), Some("7"));
|
||||||
|
assert!(rows[0].text().collect::<String>().contains("<b>Ship v0</b>"));
|
||||||
|
assert!(document.select(&selector("b")).next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// req: html_safety/002 req: view/001 req: test/005
|
||||||
|
#[test]
|
||||||
|
fn empty_todos_payload_is_rendered_by_a_hemplate_view() {
|
||||||
|
let html = render_todos(&[]);
|
||||||
|
let document = Html::parse_fragment(html.as_str());
|
||||||
|
let rows = document.select(&selector("li")).collect::<Vec<_>>();
|
||||||
|
assert_eq!(rows.len(), 1);
|
||||||
|
assert_eq!(rows[0].text().collect::<String>(), "No todos yet");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<template h-if="self.items.is_empty()">
|
||||||
|
<li>No todos yet</li>
|
||||||
|
</template>
|
||||||
|
<template h-else>
|
||||||
|
<li h-for="todo in &self.items" +data-key="todo.id">{+ todo.title +}</li>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user