|
@@ -7,19 +7,20 @@ using Spry;
|
|
|
/**
|
|
/**
|
|
|
* TodoComponent Example
|
|
* TodoComponent Example
|
|
|
*
|
|
*
|
|
|
- * Demonstrates using the Spry.Component class to build a todo list
|
|
|
|
|
- * application with component composition. Shows how to:
|
|
|
|
|
|
|
+ * Demonstrates using the Spry.Component class with IoC composition.
|
|
|
|
|
+ * Shows how to:
|
|
|
* - Build a complete CRUD interface with Components
|
|
* - 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.
|
|
|
|
|
|
|
+ * - 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]
|
|
* Usage: todo-component [port]
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
-// Simple task model for our todo list
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * TodoItem - Simple task model for our todo list.
|
|
|
|
|
+ */
|
|
|
class TodoItem : Object {
|
|
class TodoItem : Object {
|
|
|
public int id { get; set; }
|
|
public int id { get; set; }
|
|
|
public string title { get; set; }
|
|
public string title { get; set; }
|
|
@@ -32,7 +33,9 @@ class TodoItem : Object {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// In-memory todo store
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * TodoStore - In-memory todo store registered as singleton.
|
|
|
|
|
+ */
|
|
|
class TodoStore : Object {
|
|
class TodoStore : Object {
|
|
|
private Series<TodoItem> items = new Series<TodoItem>();
|
|
private Series<TodoItem> items = new Series<TodoItem>();
|
|
|
private int next_id = 1;
|
|
private int next_id = 1;
|
|
@@ -77,9 +80,6 @@ class TodoStore : Object {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Main application with todo store
|
|
|
|
|
-TodoStore todo_store;
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* EmptyListComponent - Shown when no items exist.
|
|
* EmptyListComponent - Shown when no items exist.
|
|
|
*/
|
|
*/
|
|
@@ -94,67 +94,135 @@ class EmptyListComponent : Component {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * TodoItemComponent - A single todo item.
|
|
|
|
|
|
|
+ * TodoItemComponent - A single todo item with HTMX actions.
|
|
|
*/
|
|
*/
|
|
|
class TodoItemComponent : Component {
|
|
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 {
|
|
public override string markup { get {
|
|
|
return """
|
|
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>
|
|
|
|
|
|
|
+ <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>
|
|
<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>
|
|
|
|
|
|
|
+ <button type="button" class="btn-delete" sid="delete-btn" spry-action=":Delete" spry-target="item" hx-swap="delete">Delete</button>
|
|
|
</div>
|
|
</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;
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ // 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 bool completed { set {
|
|
|
|
|
- this["toggle-btn"].text_content = value ? "Undo" : "Done";
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ 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.
|
|
* TodoListComponent - Container for todo items.
|
|
|
*/
|
|
*/
|
|
|
class TodoListComponent : Component {
|
|
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<Component> items) {
|
|
|
|
|
|
|
+ public void set_items(Enumerable<Renderable> items) {
|
|
|
set_outlet_children("items", items);
|
|
set_outlet_children("items", items);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public override string markup { get {
|
|
public override string markup { get {
|
|
|
return """
|
|
return """
|
|
|
- <div class="card">
|
|
|
|
|
|
|
+ <div class="card" id="todo-list" sid="todo-list">
|
|
|
<h2>Todo List</h2>
|
|
<h2>Todo List</h2>
|
|
|
<spry-outlet sid="items"/>
|
|
<spry-outlet sid="items"/>
|
|
|
</div>
|
|
</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.
|
|
* HeaderComponent - Page header with stats.
|
|
|
|
|
+ * Uses prepare() to fetch data from store automatically.
|
|
|
*/
|
|
*/
|
|
|
class HeaderComponent : Component {
|
|
class HeaderComponent : Component {
|
|
|
|
|
+ private TodoStore todo_store = inject<TodoStore>();
|
|
|
|
|
+
|
|
|
public override string markup { get {
|
|
public override string markup { get {
|
|
|
return """
|
|
return """
|
|
|
- <div class="card">
|
|
|
|
|
|
|
+ <div class="card" id="header" spry-global="header">
|
|
|
<h1>Todo Component Example</h1>
|
|
<h1>Todo Component Example</h1>
|
|
|
- <p>This page demonstrates <code>Spry.Component</code> for building dynamic pages.</p>
|
|
|
|
|
|
|
+ <p>This page demonstrates <code>Spry.Component</code> with IoC composition.</p>
|
|
|
<div class="stats">
|
|
<div class="stats">
|
|
|
<div class="stat">
|
|
<div class="stat">
|
|
|
<div class="stat-value" sid="total"></div>
|
|
<div class="stat-value" sid="total"></div>
|
|
@@ -169,25 +237,27 @@ class HeaderComponent : Component {
|
|
|
""";
|
|
""";
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
- public int total_tasks { set {
|
|
|
|
|
- this["total"].text_content = value.to_string();
|
|
|
|
|
- }}
|
|
|
|
|
-
|
|
|
|
|
- public int completed_tasks { set {
|
|
|
|
|
- this["completed"].text_content = value.to_string();
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ // 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.
|
|
|
|
|
|
|
+ * AddFormComponent - Form to add new todos with HTMX.
|
|
|
*/
|
|
*/
|
|
|
class AddFormComponent : Component {
|
|
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 {
|
|
public override string markup { get {
|
|
|
return """
|
|
return """
|
|
|
<div class="card">
|
|
<div class="card">
|
|
|
<h2>Add New Task</h2>
|
|
<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"/>
|
|
|
|
|
|
|
+ <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>
|
|
<button type="submit" class="btn-primary">Add Task</button>
|
|
|
</form>
|
|
</form>
|
|
|
</div>
|
|
</div>
|
|
@@ -209,15 +279,23 @@ class FeaturesComponent : Component {
|
|
|
</div>
|
|
</div>
|
|
|
<div class="feature">
|
|
<div class="feature">
|
|
|
<strong>Using Outlets:</strong>
|
|
<strong>Using Outlets:</strong>
|
|
|
- <code><spry-outlet sid="content"/></code>
|
|
|
|
|
|
|
+ <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>
|
|
|
<div class="feature">
|
|
<div class="feature">
|
|
|
- <strong>Setting Outlet Content:</strong>
|
|
|
|
|
- <code>set_outlet_child("content", child);</code>
|
|
|
|
|
|
|
+ <strong>IoC Injection:</strong>
|
|
|
|
|
+ <code>private ComponentFactory factory = inject<ComponentFactory>();</code>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="feature">
|
|
<div class="feature">
|
|
|
- <strong>Returning Results:</strong>
|
|
|
|
|
- <code>return component.to_result();</code>
|
|
|
|
|
|
|
+ <strong>Creating Components:</strong>
|
|
|
|
|
+ <code>var child = factory.create<MyComponent>();</code>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
""";
|
|
""";
|
|
@@ -244,23 +322,23 @@ class FooterComponent : Component {
|
|
|
*/
|
|
*/
|
|
|
class PageLayoutComponent : Component {
|
|
class PageLayoutComponent : Component {
|
|
|
|
|
|
|
|
- public void set_header(Component component) {
|
|
|
|
|
|
|
+ public void set_header(Renderable component) {
|
|
|
set_outlet_child("header", component);
|
|
set_outlet_child("header", component);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public void set_todo_list(Component component) {
|
|
|
|
|
|
|
+ public void set_todo_list(Renderable component) {
|
|
|
set_outlet_child("todo-list", component);
|
|
set_outlet_child("todo-list", component);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public void set_add_form(Component component) {
|
|
|
|
|
|
|
+ public void set_add_form(Renderable component) {
|
|
|
set_outlet_child("add-form", component);
|
|
set_outlet_child("add-form", component);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public void set_features(Component component) {
|
|
|
|
|
|
|
+ public void set_features(Renderable component) {
|
|
|
set_outlet_child("features", component);
|
|
set_outlet_child("features", component);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public void set_footer(Component component) {
|
|
|
|
|
|
|
+ public void set_footer(Renderable component) {
|
|
|
set_outlet_child("footer", component);
|
|
set_outlet_child("footer", component);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -272,6 +350,7 @@ class PageLayoutComponent : Component {
|
|
|
<meta charset="UTF-8"/>
|
|
<meta charset="UTF-8"/>
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
|
<title>Todo Component Example</title>
|
|
<title>Todo Component Example</title>
|
|
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
|
|
<style>
|
|
<style>
|
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
|
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); }
|
|
.card { background: white; border-radius: 8px; padding: 20px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
@@ -281,7 +360,6 @@ class PageLayoutComponent : Component {
|
|
|
.todo-item.completed { border-left-color: #ccc; opacity: 0.7; }
|
|
.todo-item.completed { border-left-color: #ccc; opacity: 0.7; }
|
|
|
.todo-item.completed .title { text-decoration: line-through; color: #888; }
|
|
.todo-item.completed .title { text-decoration: line-through; color: #888; }
|
|
|
.todo-item .title { flex: 1; margin: 0 10px; }
|
|
.todo-item .title { flex: 1; margin: 0 10px; }
|
|
|
- .todo-item form { margin: 0; }
|
|
|
|
|
.stats { display: flex; gap: 20px; margin: 10px 0; }
|
|
.stats { display: flex; gap: 20px; margin: 10px 0; }
|
|
|
.stat { background: #e8f5e9; padding: 10px 20px; border-radius: 4px; }
|
|
.stat { background: #e8f5e9; padding: 10px 20px; border-radius: 4px; }
|
|
|
.stat-value { font-size: 24px; font-weight: bold; color: #4CAF50; }
|
|
.stat-value { font-size: 24px; font-weight: bold; color: #4CAF50; }
|
|
@@ -315,29 +393,27 @@ class PageLayoutComponent : Component {
|
|
|
|
|
|
|
|
// Home page endpoint - builds the component tree
|
|
// Home page endpoint - builds the component tree
|
|
|
class HomePageEndpoint : Object, Endpoint {
|
|
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 {
|
|
public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
|
|
|
- // Create the page layout
|
|
|
|
|
- var page = new PageLayoutComponent();
|
|
|
|
|
|
|
+ // Create page layout
|
|
|
|
|
+ var page = factory.create<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 header - prepare() fetches stats from store automatically
|
|
|
|
|
+ page.set_header(factory.create<HeaderComponent>());
|
|
|
|
|
|
|
|
- // Create todo list
|
|
|
|
|
- var todo_list = new TodoListComponent();
|
|
|
|
|
|
|
+ // Create todo list - only need to set item_id, prepare() handles the rest
|
|
|
|
|
+ var todo_list = factory.create<TodoListComponent>();
|
|
|
|
|
|
|
|
var count = todo_store.count();
|
|
var count = todo_store.count();
|
|
|
if (count == 0) {
|
|
if (count == 0) {
|
|
|
- todo_list.set_items(Iterate.single<Component>(new EmptyListComponent()));
|
|
|
|
|
|
|
+ todo_list.set_items(Iterate.single<Renderable>(factory.create<EmptyListComponent>()));
|
|
|
} else {
|
|
} else {
|
|
|
- var items = new Series<Component>();
|
|
|
|
|
|
|
+ var items = new Series<Renderable>();
|
|
|
todo_store.all().iterate((item) => {
|
|
todo_store.all().iterate((item) => {
|
|
|
- var component = new TodoItemComponent();
|
|
|
|
|
|
|
+ var component = factory.create<TodoItemComponent>();
|
|
|
component.item_id = item.id;
|
|
component.item_id = item.id;
|
|
|
- component.title = item.title;
|
|
|
|
|
- component.completed = item.completed;
|
|
|
|
|
items.add(component);
|
|
items.add(component);
|
|
|
});
|
|
});
|
|
|
todo_list.set_items(items);
|
|
todo_list.set_items(items);
|
|
@@ -345,80 +421,23 @@ class HomePageEndpoint : Object, Endpoint {
|
|
|
page.set_todo_list(todo_list);
|
|
page.set_todo_list(todo_list);
|
|
|
|
|
|
|
|
// Add form
|
|
// Add form
|
|
|
- page.set_add_form(new AddFormComponent());
|
|
|
|
|
|
|
+ page.set_add_form(factory.create<AddFormComponent>());
|
|
|
|
|
|
|
|
// Features
|
|
// Features
|
|
|
- page.set_features(new FeaturesComponent());
|
|
|
|
|
|
|
+ page.set_features(factory.create<FeaturesComponent>());
|
|
|
|
|
|
|
|
// Footer
|
|
// Footer
|
|
|
- page.set_footer(new FooterComponent());
|
|
|
|
|
|
|
+ page.set_footer(factory.create<FooterComponent>());
|
|
|
|
|
|
|
|
// to_result() handles all outlet replacement automatically
|
|
// 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", "/");
|
|
|
|
|
|
|
+ return yield page.to_result();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// API endpoint that returns JSON representation of todos
|
|
// API endpoint that returns JSON representation of todos
|
|
|
class TodoJsonEndpoint : Object, Endpoint {
|
|
class TodoJsonEndpoint : Object, Endpoint {
|
|
|
|
|
+ private TodoStore todo_store = inject<TodoStore>();
|
|
|
|
|
+
|
|
|
public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
|
|
public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
|
|
|
var json_parts = new Series<string>();
|
|
var json_parts = new Series<string>();
|
|
|
json_parts.add("{\"todos\": [");
|
|
json_parts.add("{\"todos\": [");
|
|
@@ -445,21 +464,18 @@ class TodoJsonEndpoint : Object, Endpoint {
|
|
|
void main(string[] args) {
|
|
void main(string[] args) {
|
|
|
int port = args.length > 1 ? int.parse(args[1]) : 8080;
|
|
int port = args.length > 1 ? int.parse(args[1]) : 8080;
|
|
|
|
|
|
|
|
- // Initialize the todo store
|
|
|
|
|
- todo_store = new TodoStore();
|
|
|
|
|
-
|
|
|
|
|
print("═══════════════════════════════════════════════════════════════\n");
|
|
print("═══════════════════════════════════════════════════════════════\n");
|
|
|
- print(" Spry TodoComponent Example\n");
|
|
|
|
|
|
|
+ print(" Spry TodoComponent Example (IoC)\n");
|
|
|
print("═══════════════════════════════════════════════════════════════\n");
|
|
print("═══════════════════════════════════════════════════════════════\n");
|
|
|
print(" Port: %d\n", port);
|
|
print(" Port: %d\n", port);
|
|
|
print("═══════════════════════════════════════════════════════════════\n");
|
|
print("═══════════════════════════════════════════════════════════════\n");
|
|
|
print(" Endpoints:\n");
|
|
print(" Endpoints:\n");
|
|
|
print(" / - Todo list (Component-based)\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(" /api/todos - JSON API for todos\n");
|
|
|
print("═══════════════════════════════════════════════════════════════\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");
|
|
print("\nPress Ctrl+C to stop the server\n\n");
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -468,11 +484,24 @@ void main(string[] args) {
|
|
|
// Register compression components (optional, for better performance)
|
|
// Register compression components (optional, for better performance)
|
|
|
application.use_compression();
|
|
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
|
|
// Register endpoints
|
|
|
application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
|
|
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.add_endpoint<TodoJsonEndpoint>(new EndpointRoute("/api/todos"));
|
|
|
|
|
|
|
|
application.run();
|
|
application.run();
|