using Astralis; using Invercargill; using Invercargill.DataStructures; using Inversion; using Spry; /** * TodoComponent Example * * Demonstrates using the Spry.Component class to build a todo list * application with component composition. Shows how to: * - Build a complete CRUD interface with Components * - Use set_outlet_child/set_outlet_children for component composition * - Handle form submissions * * This example mirrors the Astralis DocumentBuilder example but * uses the Component class abstraction for better code organization. * * Usage: todo-component [port] */ // 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; } } // In-memory todo store 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); } } // Main application with todo store TodoStore todo_store; /** * 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. */ class TodoItemComponent : Component { public override string markup { get { return """
"""; }} public int item_id { set { this["toggle-id"].set_attribute("value", value.to_string()); this["delete-id"].set_attribute("value", value.to_string()); }} public string title { set { this["title"].text_content = value; }} public bool completed { set { this["toggle-btn"].text_content = value ? "Undo" : "Done"; }} } /** * TodoListComponent - Container for todo items. */ class TodoListComponent : Component { public void set_items(Enumerable items) { set_outlet_children("items", items); } public override string markup { get { return """

Todo List

"""; }} } /** * HeaderComponent - Page header with stats. */ class HeaderComponent : Component { public override string markup { get { return """

Todo Component Example

This page demonstrates Spry.Component for building dynamic pages.

Total Tasks
Completed
"""; }} public int total_tasks { set { this["total"].text_content = value.to_string(); }} public int completed_tasks { set { this["completed"].text_content = value.to_string(); }} } /** * AddFormComponent - Form to add new todos. */ class AddFormComponent : Component { 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:
Setting Outlet Content: set_outlet_child("content", child);
Returning Results: return component.to_result();
"""; }} } /** * 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(Component component) { set_outlet_child("header", component); } public void set_todo_list(Component component) { set_outlet_child("todo-list", component); } public void set_add_form(Component component) { set_outlet_child("add-form", component); } public void set_features(Component component) { set_outlet_child("features", component); } public void set_footer(Component 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 { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { // Create the page layout var page = new PageLayoutComponent(); // Create header with stats var header = new HeaderComponent(); header.total_tasks = todo_store.count(); header.completed_tasks = todo_store.completed_count(); page.set_header(header); // Create todo list var todo_list = new TodoListComponent(); var count = todo_store.count(); if (count == 0) { todo_list.set_items(Iterate.single(new EmptyListComponent())); } else { var items = new Series(); todo_store.all().iterate((item) => { var component = new TodoItemComponent(); component.item_id = item.id; component.title = item.title; component.completed = item.completed; items.add(component); }); todo_list.set_items(items); } page.set_todo_list(todo_list); // Add form page.set_add_form(new AddFormComponent()); // Features page.set_features(new FeaturesComponent()); // Footer page.set_footer(new FooterComponent()); // to_result() handles all outlet replacement automatically return page.to_result(); } } // Add todo endpoint class AddTodoEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { // Parse form data FormData form_data = yield FormDataParser.parse( context.request.request_body, context.request.content_type ); var title = form_data.get_field("title"); if (title != null && title.strip() != "") { todo_store.add(title.strip()); } // Redirect back to home (302 Found) return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // Toggle todo endpoint class ToggleTodoEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { FormData form_data = yield FormDataParser.parse( context.request.request_body, context.request.content_type ); var id_str = form_data.get_field("id"); if (id_str != null) { var id = int.parse(id_str); todo_store.toggle(id); } return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // Delete todo endpoint class DeleteTodoEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { FormData form_data = yield FormDataParser.parse( context.request.request_body, context.request.content_type ); var id_str = form_data.get_field("id"); if (id_str != null) { var id = int.parse(id_str); todo_store.remove(id); } return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // API endpoint that returns JSON representation of todos class TodoJsonEndpoint : Object, Endpoint { 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; // Initialize the todo store todo_store = new TodoStore(); print("═══════════════════════════════════════════════════════════════\n"); print(" Spry TodoComponent Example\n"); print("═══════════════════════════════════════════════════════════════\n"); print(" Port: %d\n", port); print("═══════════════════════════════════════════════════════════════\n"); print(" Endpoints:\n"); print(" / - Todo list (Component-based)\n"); print(" /add - Add a todo item (POST)\n"); print(" /toggle - Toggle todo completion (POST)\n"); print(" /delete - Delete a todo item (POST)\n"); print(" /api/todos - JSON API for todos\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(); // Register endpoints application.add_endpoint(new EndpointRoute("/")); application.add_endpoint(new EndpointRoute("/add")); application.add_endpoint(new EndpointRoute("/toggle")); application.add_endpoint(new EndpointRoute("/delete")); application.add_endpoint(new EndpointRoute("/api/todos")); application.run(); } catch (Error e) { printerr("Error: %s\n", e.message); Process.exit(1); } }