Files
livevue-rs/examples/todo/todo_list.rs
T
Claude 6605bf452f Fix DataStar v1 RC8 signal attribute incompatibilities
Two bugs that caused signals to not work on the frontend:

1. data-on-signal-change → data-on-signal-patch
   The data-on-signal-change attribute was removed in RC8.
   The replacement is data-on-signal-patch.

2. data-bind-newTodo="" → data-bind="newTodo"
   HTML normalises attribute names to lowercase at parse time, so
   data-bind-newTodo becomes data-bind-newtodo in the DOM. DataStar
   would then bind to signal $newtodo instead of $newTodo, breaking
   the two-way binding. The value syntax data-bind="newTodo" preserves
   the camelCase name correctly.

https://claude.ai/code/session_01JThiQbp3Bn9J1VhBpRuz4u
2026-03-06 20:13:55 +00:00

124 lines
4.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Markup> {
// 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.
// Value syntax is used instead of data-bind-newTodo because HTML
// normalises attribute names to lowercase, which would corrupt the
// camelCase signal name (newTodo → newtodo).
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__<id>"
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))
{
"×"
}
}
}
}