use crate::queries::{ListTodos, Todo}; use livevue_rs::{RenderContext, global, mangle}; use maud::{html, Markup}; /// Root page component: renders the full todo list with add form. /// /// Reads signals: /// - `newTodo` (global, server-relevant): the text input value /// /// Exercises: /// - `cx.run()` for data fetching /// - `cx.signal()` with `global()` scoping + `Signal::val` in expressions /// - `cx.signal()` with `mangle().client_only()` scoping (in todo_item) /// - DataStar `data-bind`, `data-on-click`, `data-attr` attributes /// - `@post()` actions for mutations pub async fn todo_list(cx: &mut RenderContext) -> anyhow::Result { // Fetch all todos let todos = cx.run(ListTodos).await?; // Global signal for the "new todo" input. This call does two things: // 1. Records "newTodo" as a touched signal (for demangling tracking) // 2. Returns a Signal struct whose .val is "$newTodo" for expressions let new_todo = cx.signal("newTodo", global()); let todo_count = todos.len(); let done_count = todos.iter().filter(|t| t.completed).count(); Ok(html! { div.todo-app { h1 { "LiveVue.rs — Todo List" } // Navigation link to About page (exercises tree signal + page routing) nav { span.nav-current { "Todos" } " | " button.nav-link data-on-click="$tree = {page: 'About'}; @post('/render')" { "About" } } // Add todo form. // data-bind-newTodo binds the input to the $newTodo signal. // The button uses new_todo.val ("$newTodo") in a DataStar expression. div.add-form { input type="text" placeholder="What needs to be done?" data-bind-newTodo="" ; button data-on-click="@post('/action/add_todo')" data-attr=(format!("{{disabled: {}.trim() === ''}}", new_todo.val)) { "Add" } } // Stats p.stats { (format!("{} items, {} completed", todo_count, done_count)) } // Todo items ul.todo-list { @for todo in &todos { (todo_item(cx, todo)) } } @if todos.is_empty() { p.empty { "No todos yet. Add one above!" } } } }) } /// Renders a single todo item. /// /// This is a static child component — called directly from `todo_list`, /// not resolved through the component registry. /// /// Demonstrates per-instance, client-only signal via `mangle().client_only()`. fn todo_item(cx: &mut RenderContext, todo: &Todo) -> Markup { // Per-instance, client-only signal for edit mode. // Not used in v0 rendering, but demonstrates the mangled signal API // and ensures it's tracked as touched. Produces: "_editing__" let _editing = cx.signal("editing", mangle(todo.id).client_only()); // Build class string server-side. Since we do a full rerender on every // state change, there's no need for data-class here — the server knows // whether the todo is completed. let title_class = if todo.completed { "todo-title completed" } else { "todo-title" }; html! { li.todo-item id=(format!("todo-{}", todo.id)) { // Checkbox to toggle completion. // `checked[expr]` is maud's syntax for conditional boolean attributes. input type="checkbox" checked[todo.completed] data-on-click=(format!("@post('/action/toggle_todo?id={}')", todo.id)) ; // Title with strikethrough when completed (class set server-side) span class=(title_class) { (todo.title) } // Delete button button.delete-btn data-on-click=(format!("@post('/action/delete_todo?id={}')", todo.id)) { "×" } } } }