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 +}