From cac23578e25bc3c62d4e48ee0761d013e7a47e46 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 12 Jun 2026 15:01:20 +0200 Subject: [PATCH] feat(v0): show reusable partial feedback Add a generated todo notice slot to the v0 normal path so initial render, append, replace, remove, and dynamic Vec refreshes visibly compose one reusable TodoRow partial with adjacent generated UI feedback. req: canonical_authoring/003 --- examples/v0/README.md | 2 +- examples/v0/src/main.rs | 39 ++++++++++++++++++++++++-------- examples/v0/templates/todos.heml | 1 + 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/examples/v0/README.md b/examples/v0/README.md index fe9ac5e..d11085b 100644 --- a/examples/v0/README.md +++ b/examples/v0/README.md @@ -11,7 +11,7 @@ Open . The page includes working examples for generated target objects and server-first UI commands: - counter updates -- todo form submission with one `TodoRow` hemplate partial reused by the initial page render, generated row append/replace/remove commands, and dynamic `Vec` row batches +- todo form submission with one `TodoRow` hemplate partial, a notice slot, handlers, generated row append/replace/remove commands, and dynamic `Vec` row batches reused across initial render and updates - wizard step updates - login form feedback - page swap/navigation diff --git a/examples/v0/src/main.rs b/examples/v0/src/main.rs index a875ca7..22ccc23 100644 --- a/examples/v0/src/main.rs +++ b/examples/v0/src/main.rs @@ -125,6 +125,7 @@ impl IntoHandlerFailure for AppError { struct Todos { summary: String, + notice: String, rows: Vec, } @@ -134,6 +135,8 @@ impl Hemplate for Todos { buf.push_str("
\n
\n \n \n

\n
\n\n

"); write!(buf, "{}", hemplate::HtmlEscape(&self.summary))?; + buf.push_str("

\n

"); + write!(buf, "{}", hemplate::HtmlEscape(&self.notice))?; buf.push_str("

\n\n
    \n"); for row in &self.rows { buf.push_str(" "); @@ -313,6 +316,7 @@ mod todo_handlers { todos::new_todo.error("title", "Title required"), todos::new_todo.focus("title"), todos::summary.set(todo_summary(&state.todos.lock().unwrap())), + todos::notice.set("Add a title to create a todo"), )); } if form.title.as_str() == "fail-db" { @@ -330,9 +334,10 @@ mod todo_handlers { Ok(( todos::todo_row.append(TodoRow { id: TodoId(id), - title, + title: title.clone(), }), todos::summary.set(summary), + todos::notice.set(format!("Added {title}")), todos::new_todo.clear(), )) } @@ -403,10 +408,13 @@ fn rename_todo_effect(state: Arc, form: RenameTodo) -> impl IntoEf .find(|todo| todo.id == form.id.0) .map(|todo| { todo.title = form.title.into_string(); - todos::todo_row.replace(TodoRow { - id: form.id, - title: todo.title.clone(), - }) + ( + todos::todo_row.replace(TodoRow { + id: form.id, + title: todo.title.clone(), + }), + todos::notice.set("Todo renamed"), + ) }) } @@ -420,6 +428,7 @@ fn delete_todo_effect(state: Arc, form: DeleteTodo) -> impl IntoEf ( deleted.then(|| todos::todo_row.remove(form.id)), deleted.then(|| todos::summary.set(summary)), + deleted.then(|| todos::notice.set("Todo deleted")), ) } @@ -427,6 +436,7 @@ fn todos_view(todos: &[TodoRecord]) -> Todos { // req: html_safety/002 req: view/001 req: canonical_authoring/003 Todos { summary: todo_summary(todos), + notice: String::new(), rows: todo_rows(todos), } } @@ -438,7 +448,11 @@ fn refresh_todo_rows_effect(todos: &[TodoRecord]) -> impl IntoEffect { .into_iter() .map(|row| todos::todo_row.replace(row)) .collect::>(); - (row_updates, todos::summary.set(todo_summary(todos))) + ( + row_updates, + todos::summary.set(todo_summary(todos)), + todos::notice.set("Todos refreshed"), + ) } fn todo_rows(todos: &[TodoRecord]) -> Vec { @@ -626,10 +640,11 @@ mod tests { let refresh = inspect_batch(refresh_todo_rows_effect(&todos).into_batch(ui::BUILD_FINGERPRINT)); - assert_eq!(refresh.op_count(), 3); + assert_eq!(refresh.op_count(), 4); assert!(refresh.replaces_keyed_html_containing(todos::todo_row, "7", "Ship v0")); assert!(refresh.replaces_keyed_html_containing(todos::todo_row, "8", "Write docs")); assert!(refresh.updates_text(todos::summary)); + assert!(refresh.updates_text(todos::notice)); } // req: html_safety/002 req: view/001 req: test/005 @@ -688,9 +703,11 @@ mod tests { .batch, ); assert_eq!(state.todos.lock().unwrap()[0].title, "Ship v0"); - assert_eq!(add.op_count(), 3); + assert_eq!(add.op_count(), 4); assert!(add.inserts_html_containing(todos::todo_row, "1", "Ship v0")); assert!(add.updates_text(todos::summary)); + assert!(add.updates_text(todos::notice)); + assert!(add.payload_contains("Added Ship v0")); assert!(add.resets_form(todos::new_todo)); let rename = inspect_batch( @@ -704,8 +721,9 @@ mod tests { .batch, ); assert_eq!(state.todos.lock().unwrap()[0].title, "Ship 1.0"); - assert_eq!(rename.op_count(), 1); + assert_eq!(rename.op_count(), 2); assert!(rename.replaces_keyed_html_containing(todos::todo_row, "1", "Ship 1.0")); + assert!(rename.updates_text(todos::notice)); let delete = inspect_batch( InteractionRequest::from(form(todo_row::delete_todo, &[("id", "1")])) @@ -715,9 +733,10 @@ mod tests { .batch, ); assert!(state.todos.lock().unwrap().is_empty()); - assert_eq!(delete.op_count(), 2); + assert_eq!(delete.op_count(), 3); assert!(delete.removes_key(todos::todo_row, "1")); assert!(delete.updates_text(todos::summary)); + assert!(delete.updates_text(todos::notice)); let missing_delete = inspect_batch( InteractionRequest::from(form(todo_row::delete_todo, &[("id", "99")])) diff --git a/examples/v0/templates/todos.heml b/examples/v0/templates/todos.heml index dfac2ca..ed44c80 100644 --- a/examples/v0/templates/todos.heml +++ b/examples/v0/templates/todos.heml @@ -4,6 +4,7 @@

    {+ self.summary +}

    +

    {+ self.notice +}