using Astralis; using Invercargill; using Invercargill.DataStructures; using Inversion; using Spry; /** * TodoComponent Example * * Demonstrates using the Spry.Component class with IoC composition. * Shows how to: * - Build a complete CRUD interface with Components * - Use for declarative child components * - Use get_component_child(sid) to access child components * - Use for dynamic lists (multiple items from data) * - Use inject() for dependency injection * - Use spry-action and spry-target for declarative HTMX interactions * - Use handle_action() for action handling * * Usage: todo-component [port] */ /** * TodoItem - Simple task model for our todo list. */ class TodoItem : Object { public int id { get; set; } public string title { get; set; } public bool completed { get; set; } public TodoItem(int id, string title, bool completed = false) { this.id = id; this.title = title; this.completed = completed; } } /** * TodoStore - In-memory todo store registered as singleton. */ class TodoStore : Object { private Series items = new Series(); private int next_id = 1; public TodoStore() { // Add some initial items add("Learn Spry.Component"); add("Build dynamic HTML pages"); add("Handle form submissions"); } public void add(string title) { items.add(new TodoItem(next_id++, title)); } public void toggle(int id) { items.to_immutable_buffer().iterate((item) => { if (item.id == id) { item.completed = !item.completed; } }); } public void remove(int id) { var new_items = items.to_immutable_buffer() .where(item => item.id != id) .to_series(); items = new_items; } public ImmutableBuffer all() { return items.to_immutable_buffer(); } public int count() { return (int)items.to_immutable_buffer().count(); } public int completed_count() { return (int)items.to_immutable_buffer() .count(item => item.completed); } } /** * EmptyListComponent - Shown when no items exist. */ class EmptyListComponent : Component { public override string markup { get { return """
No tasks yet! Add one below.
"""; }} } /** * TodoItemComponent - A single todo item with HTMX actions. */ class TodoItemComponent : Component { private TodoStore todo_store = inject(); private ComponentFactory factory = inject(); private HttpContext http_context = inject(); private HeaderComponent header = inject(); private int _item_id; public int item_id { set { _item_id = value; } get { return _item_id; } } public override string markup { get { return """
"""; }} // Called before serialization to prepare template with data from store public override async void prepare() throws Error { var item = todo_store.all().first_or_default(i => i.id == _item_id); if (item == null) return; this["title"].text_content = item.title; this["toggle-btn"].text_content = item.completed ? "Undo" : "Done"; if (item.completed) { this["item"].add_class("completed"); } // Set hx-vals on parent div - inherited by child buttons this["item"].set_attribute("hx-vals", @"{\"id\":$_item_id}"); } public async override void handle_action(string action) throws Error { // Get the item id from query parameters (passed via hx-vals) var id_str = http_context.request.query_params.get_any_or_default("id"); if (id_str == null) return; var id = int.parse(id_str); _item_id = id; // Set for prepare() switch (action) { case "Toggle": todo_store.toggle(id); // prepare() will be called automatically before serialization // Add header globals for OOB swap (header's prepare() fetches stats) add_globals_from(header); break; case "Delete": todo_store.remove(id); // With hx-swap="delete", we still need to return the header update add_globals_from(header); return; } } } /** * TodoListComponent - Container for todo items. */ class TodoListComponent : Component { private TodoStore todo_store = inject(); private ComponentFactory factory = inject(); private HttpContext http_context = inject(); private HeaderComponent header = inject(); public void set_items(Enumerable items) { set_outlet_children("items", items); } public override string markup { get { return """

Todo List

"""; }} public async override void handle_action(string action) throws Error { if (action == "Add") { var title = http_context.request.query_params.get_any_or_default("title"); if (title != null && title.strip() != "") { todo_store.add(title.strip()); } } // Rebuild the list - only need to set item_id, prepare() handles the rest var count = todo_store.count(); if (count == 0) { set_outlet_children("items", Iterate.single(factory.create())); } else { var items = new Series(); todo_store.all().iterate((item) => { var component = factory.create(); component.item_id = item.id; items.add(component); }); set_outlet_children("items", items); } // Add header globals for OOB swap (header's prepare() fetches stats) add_globals_from(header); } } /** * HeaderComponent - Page header with stats. * Uses prepare() to fetch data from store automatically. */ class HeaderComponent : Component { private TodoStore todo_store = inject(); public override string markup { get { return """ """; }} // Called before serialization to populate stats from store public override async void prepare() throws Error { this["total"].text_content = todo_store.count().to_string(); this["completed"].text_content = todo_store.completed_count().to_string(); } } /** * AddFormComponent - Form to add new todos with HTMX. */ class AddFormComponent : Component { private TodoStore todo_store = inject(); private ComponentFactory factory = inject(); private HttpContext http_context = inject(); public override string markup { get { return """

Add New Task

"""; }} } /** * FeaturesComponent - Shows Component features used. */ class FeaturesComponent : Component { public override string markup { get { return """

Component Features Used

Defining Components: class MyComponent : Component { public override string markup { get { return "..."; } } }
Declarative Child Components: <spry-component name="HeaderComponent" sid="header"/>
Accessing Child Components: var header = get_component_child<HeaderComponent>("header");
Using Outlets (for lists): <spry-outlet sid="items"/>
Preparing Templates: public override void prepare() { this["title"].text_content = "..."; }
Declarative HTMX Actions: <button spry-action=":Toggle" hx-vals='{"id":1}'>Toggle</button>
IoC Injection: private ComponentFactory factory = inject<ComponentFactory>();
"""; }} } /** * FooterComponent - Page footer. */ class FooterComponent : Component { public override string markup { get { return """

Built with Spry.Component | View JSON API

"""; }} } /** * TodoPage - The main page structure. * * Inherits from PageComponent to act as both a Component and Endpoint. * Uses syntax for declarative child components. * Child components are accessed via get_component_child(sid) in prepare(). */ class TodoPage : PageComponent { private TodoStore todo_store = inject(); private ComponentFactory factory = inject(); public override string markup { get { return """ Todo Component Example """; }} // Called before serialization to populate the todo list public override async void prepare() throws Error { // Get the TodoListComponent child and populate it var todo_list = get_component_child("todo-list"); var count = todo_store.count(); if (count == 0) { todo_list.set_items(Iterate.single(factory.create())); } else { var items = new Series(); todo_store.all().iterate((item) => { var component = factory.create(); component.item_id = item.id; items.add(component); }); todo_list.set_items(items); } } } // API endpoint that returns JSON representation of todos class TodoJsonEndpoint : Object, Endpoint { private TodoStore todo_store = inject(); public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { var json_parts = new Series(); json_parts.add("{\"todos\": ["); bool first = true; todo_store.all().iterate((item) => { if (!first) json_parts.add(","); var completed_str = item.completed ? "true" : "false"; var escaped_title = item.title.replace("\"", "\\\""); json_parts.add(@"{\"id\":$(item.id),\"title\":\"$escaped_title\",\"completed\":$completed_str}"); first = false; }); json_parts.add("]}"); var json = json_parts.to_immutable_buffer() .aggregate("", (acc, s) => acc + s); return new HttpStringResult(json) .set_header("Content-Type", "application/json"); } } void main(string[] args) { int port = args.length > 1 ? int.parse(args[1]) : 8080; print("═══════════════════════════════════════════════════════════════\n"); print(" Spry TodoComponent Example (IoC)\n"); print("═══════════════════════════════════════════════════════════════\n"); print(" Port: %d\n", port); print("═══════════════════════════════════════════════════════════════\n"); print(" Endpoints:\n"); print(" / - Todo list (Component-based)\n"); print(" /api/todos - JSON API for todos\n"); print("═══════════════════════════════════════════════════════════════\n"); print(" HTMX Actions (via SpryModule):\n"); print(" Add, Toggle, Delete - Dynamic updates\n"); print("═══════════════════════════════════════════════════════════════\n"); print("\nPress Ctrl+C to stop the server\n\n"); try { var application = new WebApplication(port); // Register compression components (optional, for better performance) application.use_compression(); // Add Spry module for automatic HTMX endpoint registration application.add_module(); // Register the todo store as singleton application.add_singleton(); // Configure Spry components and pages var spry_cfg = application.configure_with(); // Register child components spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_component(); // Register the page (acts as both Component and Endpoint) spry_cfg.add_page(new EndpointRoute("/")); // Register JSON API endpoint application.add_endpoint(new EndpointRoute("/api/todos")); application.run(); } catch (Error e) { printerr("Error: %s\n", e.message); Process.exit(1); } }