TodoComponent.vala 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. using Inversion;
  5. using Spry;
  6. /**
  7. * TodoComponent Example
  8. *
  9. * Demonstrates using the Spry.Component class to build a todo list
  10. * application with component composition. Shows how to:
  11. * - Build a complete CRUD interface with Components
  12. * - Use set_outlet_child/set_outlet_children for component composition
  13. * - Handle form submissions
  14. *
  15. * This example mirrors the Astralis DocumentBuilder example but
  16. * uses the Component class abstraction for better code organization.
  17. *
  18. * Usage: todo-component [port]
  19. */
  20. // Simple task model for our todo list
  21. class TodoItem : Object {
  22. public int id { get; set; }
  23. public string title { get; set; }
  24. public bool completed { get; set; }
  25. public TodoItem(int id, string title, bool completed = false) {
  26. this.id = id;
  27. this.title = title;
  28. this.completed = completed;
  29. }
  30. }
  31. // In-memory todo store
  32. class TodoStore : Object {
  33. private Series<TodoItem> items = new Series<TodoItem>();
  34. private int next_id = 1;
  35. public TodoStore() {
  36. // Add some initial items
  37. add("Learn Spry.Component");
  38. add("Build dynamic HTML pages");
  39. add("Handle form submissions");
  40. }
  41. public void add(string title) {
  42. items.add(new TodoItem(next_id++, title));
  43. }
  44. public void toggle(int id) {
  45. items.to_immutable_buffer().iterate((item) => {
  46. if (item.id == id) {
  47. item.completed = !item.completed;
  48. }
  49. });
  50. }
  51. public void remove(int id) {
  52. var new_items = items.to_immutable_buffer()
  53. .where(item => item.id != id)
  54. .to_series();
  55. items = new_items;
  56. }
  57. public ImmutableBuffer<TodoItem> all() {
  58. return items.to_immutable_buffer();
  59. }
  60. public int count() {
  61. return (int)items.to_immutable_buffer().count();
  62. }
  63. public int completed_count() {
  64. return (int)items.to_immutable_buffer()
  65. .count(item => item.completed);
  66. }
  67. }
  68. // Main application with todo store
  69. TodoStore todo_store;
  70. /**
  71. * EmptyListComponent - Shown when no items exist.
  72. */
  73. class EmptyListComponent : Component {
  74. public override string markup { get {
  75. return """
  76. <div class="empty">
  77. No tasks yet! Add one below.
  78. </div>
  79. """;
  80. }}
  81. }
  82. /**
  83. * TodoItemComponent - A single todo item.
  84. */
  85. class TodoItemComponent : Component {
  86. public override string markup { get {
  87. return """
  88. <div class="todo-item">
  89. <form method="POST" action="/toggle" style="display: inline;">
  90. <input type="hidden" name="id" sid="toggle-id"/>
  91. <button type="submit" class="btn-toggle" sid="toggle-btn"></button>
  92. </form>
  93. <span class="title" sid="title"></span>
  94. <form method="POST" action="/delete" style="display: inline;">
  95. <input type="hidden" name="id" sid="delete-id"/>
  96. <button type="submit" class="btn-delete">Delete</button>
  97. </form>
  98. </div>
  99. """;
  100. }}
  101. public int item_id { set {
  102. this["toggle-id"].set_attribute("value", value.to_string());
  103. this["delete-id"].set_attribute("value", value.to_string());
  104. }}
  105. public string title { set {
  106. this["title"].text_content = value;
  107. }}
  108. public bool completed { set {
  109. this["toggle-btn"].text_content = value ? "Undo" : "Done";
  110. }}
  111. }
  112. /**
  113. * TodoListComponent - Container for todo items.
  114. */
  115. class TodoListComponent : Component {
  116. public void set_items(Enumerable<Component> items) {
  117. set_outlet_children("items", items);
  118. }
  119. public override string markup { get {
  120. return """
  121. <div class="card">
  122. <h2>Todo List</h2>
  123. <spry-outlet sid="items"/>
  124. </div>
  125. """;
  126. }}
  127. }
  128. /**
  129. * HeaderComponent - Page header with stats.
  130. */
  131. class HeaderComponent : Component {
  132. public override string markup { get {
  133. return """
  134. <div class="card">
  135. <h1>Todo Component Example</h1>
  136. <p>This page demonstrates <code>Spry.Component</code> for building dynamic pages.</p>
  137. <div class="stats">
  138. <div class="stat">
  139. <div class="stat-value" sid="total"></div>
  140. <div class="stat-label">Total Tasks</div>
  141. </div>
  142. <div class="stat">
  143. <div class="stat-value" sid="completed"></div>
  144. <div class="stat-label">Completed</div>
  145. </div>
  146. </div>
  147. </div>
  148. """;
  149. }}
  150. public int total_tasks { set {
  151. this["total"].text_content = value.to_string();
  152. }}
  153. public int completed_tasks { set {
  154. this["completed"].text_content = value.to_string();
  155. }}
  156. }
  157. /**
  158. * AddFormComponent - Form to add new todos.
  159. */
  160. class AddFormComponent : Component {
  161. public override string markup { get {
  162. return """
  163. <div class="card">
  164. <h2>Add New Task</h2>
  165. <form method="POST" action="/add" style="display: flex; gap: 10px;">
  166. <input type="text" name="title" placeholder="Enter a new task..." required="required"/>
  167. <button type="submit" class="btn-primary">Add Task</button>
  168. </form>
  169. </div>
  170. """;
  171. }}
  172. }
  173. /**
  174. * FeaturesComponent - Shows Component features used.
  175. */
  176. class FeaturesComponent : Component {
  177. public override string markup { get {
  178. return """
  179. <div class="card">
  180. <h2>Component Features Used</h2>
  181. <div class="feature">
  182. <strong>Defining Components:</strong>
  183. <code>class MyComponent : Component { public override string markup { get { return "..."; } } }</code>
  184. </div>
  185. <div class="feature">
  186. <strong>Using Outlets:</strong>
  187. <code><spry-outlet sid="content"/></code>
  188. </div>
  189. <div class="feature">
  190. <strong>Setting Outlet Content:</strong>
  191. <code>set_outlet_child("content", child);</code>
  192. </div>
  193. <div class="feature">
  194. <strong>Returning Results:</strong>
  195. <code>return component.to_result();</code>
  196. </div>
  197. </div>
  198. """;
  199. }}
  200. }
  201. /**
  202. * FooterComponent - Page footer.
  203. */
  204. class FooterComponent : Component {
  205. public override string markup { get {
  206. return """
  207. <div class="card" style="text-align: center; color: #666;">
  208. <p>Built with <code>Spry.Component</code> |
  209. <a href="/api/todos">View JSON API</a>
  210. </p>
  211. </div>
  212. """;
  213. }}
  214. }
  215. /**
  216. * PageLayoutComponent - The main page structure.
  217. */
  218. class PageLayoutComponent : Component {
  219. public void set_header(Component component) {
  220. set_outlet_child("header", component);
  221. }
  222. public void set_todo_list(Component component) {
  223. set_outlet_child("todo-list", component);
  224. }
  225. public void set_add_form(Component component) {
  226. set_outlet_child("add-form", component);
  227. }
  228. public void set_features(Component component) {
  229. set_outlet_child("features", component);
  230. }
  231. public void set_footer(Component component) {
  232. set_outlet_child("footer", component);
  233. }
  234. public override string markup { get {
  235. return """
  236. <!DOCTYPE html>
  237. <html>
  238. <head>
  239. <meta charset="UTF-8"/>
  240. <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  241. <title>Todo Component Example</title>
  242. <style>
  243. body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
  244. .card { background: white; border-radius: 8px; padding: 20px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
  245. h1 { color: #333; margin-top: 0; }
  246. h2 { color: #555; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
  247. .todo-item { display: flex; align-items: center; padding: 10px; margin: 5px 0; background: #fafafa; border-radius: 4px; border-left: 4px solid #4CAF50; }
  248. .todo-item.completed { border-left-color: #ccc; opacity: 0.7; }
  249. .todo-item.completed .title { text-decoration: line-through; color: #888; }
  250. .todo-item .title { flex: 1; margin: 0 10px; }
  251. .todo-item form { margin: 0; }
  252. .stats { display: flex; gap: 20px; margin: 10px 0; }
  253. .stat { background: #e8f5e9; padding: 10px 20px; border-radius: 4px; }
  254. .stat-value { font-size: 24px; font-weight: bold; color: #4CAF50; }
  255. .stat-label { font-size: 12px; color: #666; }
  256. input[type="text"] { padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1; }
  257. button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
  258. .btn-primary { background: #4CAF50; color: white; }
  259. .btn-primary:hover { background: #45a049; }
  260. .btn-toggle { background: #2196F3; color: white; padding: 5px 10px; font-size: 12px; }
  261. .btn-delete { background: #f44336; color: white; padding: 5px 10px; font-size: 12px; }
  262. code { background: #e8e8e8; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
  263. pre { background: #263238; color: #aed581; padding: 15px; border-radius: 4px; overflow-x: auto; }
  264. .feature { margin: 15px 0; }
  265. .feature code { display: block; background: #f5f5f5; padding: 10px; margin: 5px 0; }
  266. a { color: #2196F3; text-decoration: none; }
  267. a:hover { text-decoration: underline; }
  268. .empty { color: #999; font-style: italic; padding: 20px; text-align: center; }
  269. </style>
  270. </head>
  271. <body>
  272. <spry-outlet sid="header"/>
  273. <spry-outlet sid="todo-list"/>
  274. <spry-outlet sid="add-form"/>
  275. <spry-outlet sid="features"/>
  276. <spry-outlet sid="footer"/>
  277. </body>
  278. </html>
  279. """;
  280. }}
  281. }
  282. // Home page endpoint - builds the component tree
  283. class HomePageEndpoint : Object, Endpoint {
  284. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  285. // Create the page layout
  286. var page = new PageLayoutComponent();
  287. // Create header with stats
  288. var header = new HeaderComponent();
  289. header.total_tasks = todo_store.count();
  290. header.completed_tasks = todo_store.completed_count();
  291. page.set_header(header);
  292. // Create todo list
  293. var todo_list = new TodoListComponent();
  294. var count = todo_store.count();
  295. if (count == 0) {
  296. todo_list.set_items(Iterate.single<Component>(new EmptyListComponent()));
  297. } else {
  298. var items = new Series<Component>();
  299. todo_store.all().iterate((item) => {
  300. var component = new TodoItemComponent();
  301. component.item_id = item.id;
  302. component.title = item.title;
  303. component.completed = item.completed;
  304. items.add(component);
  305. });
  306. todo_list.set_items(items);
  307. }
  308. page.set_todo_list(todo_list);
  309. // Add form
  310. page.set_add_form(new AddFormComponent());
  311. // Features
  312. page.set_features(new FeaturesComponent());
  313. // Footer
  314. page.set_footer(new FooterComponent());
  315. // to_result() handles all outlet replacement automatically
  316. return page.to_result();
  317. }
  318. }
  319. // Add todo endpoint
  320. class AddTodoEndpoint : Object, Endpoint {
  321. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  322. // Parse form data
  323. FormData form_data = yield FormDataParser.parse(
  324. context.request.request_body,
  325. context.request.content_type
  326. );
  327. var title = form_data.get_field("title");
  328. if (title != null && title.strip() != "") {
  329. todo_store.add(title.strip());
  330. }
  331. // Redirect back to home (302 Found)
  332. return new HttpStringResult("", (StatusCode)302)
  333. .set_header("Location", "/");
  334. }
  335. }
  336. // Toggle todo endpoint
  337. class ToggleTodoEndpoint : Object, Endpoint {
  338. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  339. FormData form_data = yield FormDataParser.parse(
  340. context.request.request_body,
  341. context.request.content_type
  342. );
  343. var id_str = form_data.get_field("id");
  344. if (id_str != null) {
  345. var id = int.parse(id_str);
  346. todo_store.toggle(id);
  347. }
  348. return new HttpStringResult("", (StatusCode)302)
  349. .set_header("Location", "/");
  350. }
  351. }
  352. // Delete todo endpoint
  353. class DeleteTodoEndpoint : Object, Endpoint {
  354. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  355. FormData form_data = yield FormDataParser.parse(
  356. context.request.request_body,
  357. context.request.content_type
  358. );
  359. var id_str = form_data.get_field("id");
  360. if (id_str != null) {
  361. var id = int.parse(id_str);
  362. todo_store.remove(id);
  363. }
  364. return new HttpStringResult("", (StatusCode)302)
  365. .set_header("Location", "/");
  366. }
  367. }
  368. // API endpoint that returns JSON representation of todos
  369. class TodoJsonEndpoint : Object, Endpoint {
  370. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  371. var json_parts = new Series<string>();
  372. json_parts.add("{\"todos\": [");
  373. bool first = true;
  374. todo_store.all().iterate((item) => {
  375. if (!first) json_parts.add(",");
  376. var completed_str = item.completed ? "true" : "false";
  377. var escaped_title = item.title.replace("\"", "\\\"");
  378. json_parts.add(@"{\"id\":$(item.id),\"title\":\"$escaped_title\",\"completed\":$completed_str}");
  379. first = false;
  380. });
  381. json_parts.add("]}");
  382. var json = json_parts.to_immutable_buffer()
  383. .aggregate<string>("", (acc, s) => acc + s);
  384. return new HttpStringResult(json)
  385. .set_header("Content-Type", "application/json");
  386. }
  387. }
  388. void main(string[] args) {
  389. int port = args.length > 1 ? int.parse(args[1]) : 8080;
  390. // Initialize the todo store
  391. todo_store = new TodoStore();
  392. print("═══════════════════════════════════════════════════════════════\n");
  393. print(" Spry TodoComponent Example\n");
  394. print("═══════════════════════════════════════════════════════════════\n");
  395. print(" Port: %d\n", port);
  396. print("═══════════════════════════════════════════════════════════════\n");
  397. print(" Endpoints:\n");
  398. print(" / - Todo list (Component-based)\n");
  399. print(" /add - Add a todo item (POST)\n");
  400. print(" /toggle - Toggle todo completion (POST)\n");
  401. print(" /delete - Delete a todo item (POST)\n");
  402. print(" /api/todos - JSON API for todos\n");
  403. print("═══════════════════════════════════════════════════════════════\n");
  404. print("\nPress Ctrl+C to stop the server\n\n");
  405. try {
  406. var application = new WebApplication(port);
  407. // Register compression components (optional, for better performance)
  408. application.use_compression();
  409. // Register endpoints
  410. application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
  411. application.add_endpoint<AddTodoEndpoint>(new EndpointRoute("/add"));
  412. application.add_endpoint<ToggleTodoEndpoint>(new EndpointRoute("/toggle"));
  413. application.add_endpoint<DeleteTodoEndpoint>(new EndpointRoute("/delete"));
  414. application.add_endpoint<TodoJsonEndpoint>(new EndpointRoute("/api/todos"));
  415. application.run();
  416. } catch (Error e) {
  417. printerr("Error: %s\n", e.message);
  418. Process.exit(1);
  419. }
  420. }