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
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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))
|
||||
{
|
||||
"×"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user