| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- 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<TodoItem> items = new Series<TodoItem>();
- 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<TodoItem> 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 """
- <div class="empty">
- No tasks yet! Add one below.
- </div>
- """;
- }}
- }
- /**
- * TodoItemComponent - A single todo item.
- */
- class TodoItemComponent : Component {
- public override string markup { get {
- return """
- <div class="todo-item">
- <form method="POST" action="/toggle" style="display: inline;">
- <input type="hidden" name="id" sid="toggle-id"/>
- <button type="submit" class="btn-toggle" sid="toggle-btn"></button>
- </form>
- <span class="title" sid="title"></span>
- <form method="POST" action="/delete" style="display: inline;">
- <input type="hidden" name="id" sid="delete-id"/>
- <button type="submit" class="btn-delete">Delete</button>
- </form>
- </div>
- """;
- }}
-
- 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<Component> items) {
- set_outlet_children("items", items);
- }
-
- public override string markup { get {
- return """
- <div class="card">
- <h2>Todo List</h2>
- <spry-outlet sid="items"/>
- </div>
- """;
- }}
- }
- /**
- * HeaderComponent - Page header with stats.
- */
- class HeaderComponent : Component {
- public override string markup { get {
- return """
- <div class="card">
- <h1>Todo Component Example</h1>
- <p>This page demonstrates <code>Spry.Component</code> for building dynamic pages.</p>
- <div class="stats">
- <div class="stat">
- <div class="stat-value" sid="total"></div>
- <div class="stat-label">Total Tasks</div>
- </div>
- <div class="stat">
- <div class="stat-value" sid="completed"></div>
- <div class="stat-label">Completed</div>
- </div>
- </div>
- </div>
- """;
- }}
-
- 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 """
- <div class="card">
- <h2>Add New Task</h2>
- <form method="POST" action="/add" style="display: flex; gap: 10px;">
- <input type="text" name="title" placeholder="Enter a new task..." required="required"/>
- <button type="submit" class="btn-primary">Add Task</button>
- </form>
- </div>
- """;
- }}
- }
- /**
- * FeaturesComponent - Shows Component features used.
- */
- class FeaturesComponent : Component {
- public override string markup { get {
- return """
- <div class="card">
- <h2>Component Features Used</h2>
- <div class="feature">
- <strong>Defining Components:</strong>
- <code>class MyComponent : Component { public override string markup { get { return "..."; } } }</code>
- </div>
- <div class="feature">
- <strong>Using Outlets:</strong>
- <code><spry-outlet sid="content"/></code>
- </div>
- <div class="feature">
- <strong>Setting Outlet Content:</strong>
- <code>set_outlet_child("content", child);</code>
- </div>
- <div class="feature">
- <strong>Returning Results:</strong>
- <code>return component.to_result();</code>
- </div>
- </div>
- """;
- }}
- }
- /**
- * FooterComponent - Page footer.
- */
- class FooterComponent : Component {
- public override string markup { get {
- return """
- <div class="card" style="text-align: center; color: #666;">
- <p>Built with <code>Spry.Component</code> |
- <a href="/api/todos">View JSON API</a>
- </p>
- </div>
- """;
- }}
- }
- /**
- * 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 """
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
- <title>Todo Component Example</title>
- <style>
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
- .card { background: white; border-radius: 8px; padding: 20px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
- h1 { color: #333; margin-top: 0; }
- h2 { color: #555; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
- .todo-item { display: flex; align-items: center; padding: 10px; margin: 5px 0; background: #fafafa; border-radius: 4px; border-left: 4px solid #4CAF50; }
- .todo-item.completed { border-left-color: #ccc; opacity: 0.7; }
- .todo-item.completed .title { text-decoration: line-through; color: #888; }
- .todo-item .title { flex: 1; margin: 0 10px; }
- .todo-item form { margin: 0; }
- .stats { display: flex; gap: 20px; margin: 10px 0; }
- .stat { background: #e8f5e9; padding: 10px 20px; border-radius: 4px; }
- .stat-value { font-size: 24px; font-weight: bold; color: #4CAF50; }
- .stat-label { font-size: 12px; color: #666; }
- input[type="text"] { padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1; }
- button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
- .btn-primary { background: #4CAF50; color: white; }
- .btn-primary:hover { background: #45a049; }
- .btn-toggle { background: #2196F3; color: white; padding: 5px 10px; font-size: 12px; }
- .btn-delete { background: #f44336; color: white; padding: 5px 10px; font-size: 12px; }
- code { background: #e8e8e8; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
- pre { background: #263238; color: #aed581; padding: 15px; border-radius: 4px; overflow-x: auto; }
- .feature { margin: 15px 0; }
- .feature code { display: block; background: #f5f5f5; padding: 10px; margin: 5px 0; }
- a { color: #2196F3; text-decoration: none; }
- a:hover { text-decoration: underline; }
- .empty { color: #999; font-style: italic; padding: 20px; text-align: center; }
- </style>
- </head>
- <body>
- <spry-outlet sid="header"/>
- <spry-outlet sid="todo-list"/>
- <spry-outlet sid="add-form"/>
- <spry-outlet sid="features"/>
- <spry-outlet sid="footer"/>
- </body>
- </html>
- """;
- }}
- }
- // 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<Component>(new EmptyListComponent()));
- } else {
- var items = new Series<Component>();
- 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<string>();
- 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<string>("", (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<HomePageEndpoint>(new EndpointRoute("/"));
- application.add_endpoint<AddTodoEndpoint>(new EndpointRoute("/add"));
- application.add_endpoint<ToggleTodoEndpoint>(new EndpointRoute("/toggle"));
- application.add_endpoint<DeleteTodoEndpoint>(new EndpointRoute("/delete"));
- application.add_endpoint<TodoJsonEndpoint>(new EndpointRoute("/api/todos"));
-
- application.run();
-
- } catch (Error e) {
- printerr("Error: %s\n", e.message);
- Process.exit(1);
- }
- }
|