TodoComponent.vala 17 KB

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