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 = [
|
||||
"axum",
|
||||
"futures-util",
|
||||
"hemplate",
|
||||
"scraper",
|
||||
"slhx",
|
||||
"slhx-axum",
|
||||
"slhx-build",
|
||||
|
||||
@@ -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]
|
||||
|
||||
+60
-17
@@ -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::{push, IntoEffect, SafeHtml};
|
||||
use slhx_axum::{runtime_js, sse, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
|
||||
use slhx_v0_examples::ui;
|
||||
@@ -25,6 +26,17 @@ struct Todo {
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct TodoItems {
|
||||
items: Vec<TodoItem>,
|
||||
}
|
||||
|
||||
struct TodoItem {
|
||||
id: u64,
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let state = Arc::new(ExampleState::default());
|
||||
@@ -116,7 +128,7 @@ fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
||||
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"),
|
||||
)
|
||||
}
|
||||
@@ -189,22 +201,53 @@ fn shell(body: String) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_todos(todos: &[Todo]) -> String {
|
||||
if todos.is_empty() {
|
||||
return "<li>No todos yet</li>".into();
|
||||
}
|
||||
todos
|
||||
.iter()
|
||||
.map(|todo| format!(r#"<li data-key="{}">{}</li>"#, todo.id, escape_html(&todo.title)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
fn render_todos(todos: &[Todo]) -> SafeHtml {
|
||||
// req: html_safety/002 req: view/001
|
||||
render_html(&TodoItems {
|
||||
items: todos
|
||||
.iter()
|
||||
.map(|todo| TodoItem {
|
||||
id: todo.id,
|
||||
title: todo.title.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
fn render_html(template: &impl Hemplate) -> SafeHtml {
|
||||
SafeHtml::trusted(template.render().expect("v0 hemplate partial renders"))
|
||||
}
|
||||
|
||||
#[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 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