| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- 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<T>() 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<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);
- }
- }
- /**
- * 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 with HTMX actions.
- */
- class TodoItemComponent : Component {
- private TodoStore todo_store = inject<TodoStore>();
- private ComponentFactory factory = inject<ComponentFactory>();
- private HttpContext http_context = inject<HttpContext>();
- private HeaderComponent header = inject<HeaderComponent>();
-
- private int _item_id;
- public int item_id {
- set {
- _item_id = value;
- }
- get {
- return _item_id;
- }
- }
-
- public override string markup { get {
- return """
- <div class="todo-item" sid="item">
- <button type="button" class="btn-toggle" sid="toggle-btn" spry-action=":Toggle" spry-target="item" hx-swap="outerHTML"></button>
- <span class="title" sid="title"></span>
- <button type="button" class="btn-delete" sid="delete-btn" spry-action=":Delete" spry-target="item" hx-swap="delete">Delete</button>
- </div>
- """;
- }}
-
- // 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<TodoStore>();
- private ComponentFactory factory = inject<ComponentFactory>();
- private HttpContext http_context = inject<HttpContext>();
- private HeaderComponent header = inject<HeaderComponent>();
-
- public void set_items(Enumerable<Renderable> items) {
- set_outlet_children("items", items);
- }
-
- public override string markup { get {
- return """
- <div class="card" id="todo-list" sid="todo-list">
- <h2>Todo List</h2>
- <spry-outlet sid="items"/>
- </div>
- """;
- }}
-
- 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<Renderable>(factory.create<EmptyListComponent>()));
- } else {
- var items = new Series<Renderable>();
- todo_store.all().iterate((item) => {
- var component = factory.create<TodoItemComponent>();
- 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<TodoStore>();
-
- public override string markup { get {
- return """
- <div class="card" id="header" spry-global="header">
- <h1>Todo Component Example</h1>
- <p>This page demonstrates <code>Spry.Component</code> with IoC composition.</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>
- """;
- }}
-
- // 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<TodoStore>();
- private ComponentFactory factory = inject<ComponentFactory>();
- private HttpContext http_context = inject<HttpContext>();
-
- public override string markup { get {
- return """
- <div class="card">
- <h2>Add New Task</h2>
- <form sid="form" spry-action="TodoListComponent:Add" hx-target="#todo-list" hx-swap="outerHTML" style="display: flex; gap: 10px;">
- <input type="text" name="title" placeholder="Enter a new task..." required="required" sid="title-input"/>
- <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>Preparing Templates:</strong>
- <code>public override void prepare() { this["title"].text_content = "..."; }</code>
- </div>
- <div class="feature">
- <strong>Declarative HTMX Actions:</strong>
- <code><button spry-action=":Toggle" hx-vals='{"id":1}'>Toggle</button></code>
- </div>
- <div class="feature">
- <strong>IoC Injection:</strong>
- <code>private ComponentFactory factory = inject<ComponentFactory>();</code>
- </div>
- <div class="feature">
- <strong>Creating Components:</strong>
- <code>var child = factory.create<MyComponent>();</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(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 """
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
- <title>Todo Component Example</title>
- <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
- <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; }
- .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 {
- private TodoStore todo_store = inject<TodoStore>();
- private ComponentFactory factory = inject<ComponentFactory>();
-
- public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
- // Create page layout
- var page = factory.create<PageLayoutComponent>();
-
- // Create header - prepare() fetches stats from store automatically
- page.set_header(factory.create<HeaderComponent>());
-
- // Create todo list - only need to set item_id, prepare() handles the rest
- var todo_list = factory.create<TodoListComponent>();
-
- var count = todo_store.count();
- if (count == 0) {
- todo_list.set_items(Iterate.single<Renderable>(factory.create<EmptyListComponent>()));
- } else {
- var items = new Series<Renderable>();
- todo_store.all().iterate((item) => {
- var component = factory.create<TodoItemComponent>();
- 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<AddFormComponent>());
-
- // Features
- page.set_features(factory.create<FeaturesComponent>());
-
- // Footer
- page.set_footer(factory.create<FooterComponent>());
-
- // 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<TodoStore>();
-
- 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;
-
- 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<SpryModule>();
-
- // Register the todo store as singleton
- application.add_singleton<TodoStore>();
-
- // Register components as transient (created via factory)
- application.add_transient<EmptyListComponent>();
- application.add_transient<TodoItemComponent>();
- application.add_transient<TodoListComponent>();
- application.add_transient<HeaderComponent>();
- application.add_transient<AddFormComponent>();
- application.add_transient<FeaturesComponent>();
- application.add_transient<FooterComponent>();
- application.add_transient<PageLayoutComponent>();
-
- // Register endpoints
- application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
- application.add_endpoint<TodoJsonEndpoint>(new EndpointRoute("/api/todos"));
-
- application.run();
-
- } catch (Error e) {
- printerr("Error: %s\n", e.message);
- Process.exit(1);
- }
- }
|