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 inject() for dependency injection * - Use ComponentFactory to create component instances * - 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 "..."; } } }
Using Outlets: <spry-outlet sid="content"/>
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();
Creating Components: var child = factory.create<MyComponent>();
"""; }} } /** * FooterComponent - Page footer. */ class FooterComponent : Component { public override string markup { get { return """

Built with Spry.Component | View JSON API

"""; }} } /** * PageLayoutComponent - The main page structure. */ class PageLayoutComponent : Component { public void set_header(Renderable component) { set_outlet_child("header", component); } public void set_todo_list(Renderable component) { set_outlet_child("todo-list", component); } public void set_add_form(Renderable component) { set_outlet_child("add-form", component); } public void set_features(Renderable component) { set_outlet_child("features", component); } public void set_footer(Renderable component) { set_outlet_child("footer", component); } public override string markup { get { return """ Todo Component Example """; }} } // Home page endpoint - builds the component tree class HomePageEndpoint : Object, Endpoint { private TodoStore todo_store = inject(); private ComponentFactory factory = inject(); public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { // Create page layout var page = factory.create(); // Create header - prepare() fetches stats from store automatically page.set_header(factory.create()); // Create todo list - only need to set item_id, prepare() handles the rest var todo_list = factory.create(); 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); } page.set_todo_list(todo_list); // Add form page.set_add_form(factory.create()); // Features page.set_features(factory.create()); // Footer page.set_footer(factory.create()); // to_result() handles all outlet replacement automatically return yield page.to_result(); } } // 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(); // Register components as transient (created via factory) application.add_transient(); application.add_transient(); application.add_transient(); application.add_transient(); application.add_transient(); application.add_transient(); application.add_transient(); application.add_transient(); // Register endpoints application.add_endpoint(new EndpointRoute("/")); application.add_endpoint(new EndpointRoute("/api/todos")); application.run(); } catch (Error e) { printerr("Error: %s\n", e.message); Process.exit(1); } }