Files
livevue-rs/examples/todo/todo_list.rs
T
Claude 8535d39012 Refactor: untangle framework library from todo example
The codebase is now split into two clear layers:

Framework library (src/):
- lib.rs — public API surface, re-exports all framework types
- signal.rs — SignalOpts, Signal, SignalStore, mangle/global/client_only helpers
- query.rs — Query trait, CacheKey, query_one!/query_all! macros
  (macro paths updated from $crate::framework::* to $crate::*)
- context.rs — RenderContext, ComponentTree, ComponentNode
- connection.rs — ConnectionManager, SubscriptionRegistry, ConnectionState
- sse.rs — format_patch_elements, format_patch_signals
- server.rs — AppState, SharedState, sse_handler (pure framework plumbing)

Todo example (examples/todo/):
- main.rs — SQLite setup, seeding, server bootstrap
- app.rs — router, document_shell, resolve_and_render, render_page,
  action_then_render, and all HTTP handlers
- queries.rs — Todo model, ListTodos/GetTodo queries, mutation helpers
- todo_list.rs — TodoList page component
- about.rs — About page component

Run the example independently with: cargo run --example todo

https://claude.ai/code/session_01JThiQbp3Bn9J1VhBpRuz4u
2026-03-06 16:51:53 +00:00

122 lines
4.1 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.
// 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__<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))
{
"×"
}
}
}
}