CounterComponent.vala 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. using Inversion;
  5. using Spry;
  6. /**
  7. * CounterComponent Example
  8. *
  9. * Demonstrates using the Spry.Component class with IoC composition and HTMX.
  10. * Shows how to:
  11. * - Define Component subclasses with embedded HTML markup
  12. * - Use spry-outlet elements for dynamic content injection
  13. * - Use inject<T>() for dependency injection
  14. * - Use ComponentFactory to create component instances
  15. * - Use spry-action and spry-target for declarative HTMX interactions
  16. * - Use handle_action() for action handling
  17. *
  18. * Usage: counter-component [port]
  19. */
  20. /**
  21. * AppState - Application state registered as singleton.
  22. */
  23. class AppState : Object {
  24. public int counter { get; set; }
  25. public int total_changes { get; set; }
  26. public string last_action { get; set; }
  27. public DateTime last_update { get; set; }
  28. public AppState() {
  29. counter = 0;
  30. total_changes = 0;
  31. last_action = "Initialized";
  32. last_update = new DateTime.now_local();
  33. }
  34. public void increment() {
  35. counter++;
  36. total_changes++;
  37. last_action = "Incremented";
  38. last_update = new DateTime.now_local();
  39. }
  40. public void decrement() {
  41. counter--;
  42. total_changes++;
  43. last_action = "Decremented";
  44. last_update = new DateTime.now_local();
  45. }
  46. public void reset() {
  47. counter = 0;
  48. total_changes++;
  49. last_action = "Reset";
  50. last_update = new DateTime.now_local();
  51. }
  52. }
  53. /**
  54. * CSS content for the counter page.
  55. * Served as a FastResource for optimal performance with pre-compression.
  56. */
  57. private const string COUNTER_CSS = """
  58. body {
  59. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  60. max-width: 700px;
  61. margin: 0 auto;
  62. padding: 20px;
  63. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  64. min-height: 100vh;
  65. }
  66. .card {
  67. background: white;
  68. border-radius: 12px;
  69. padding: 25px;
  70. margin: 15px 0;
  71. box-shadow: 0 10px 40px rgba(0,0,0,0.2);
  72. }
  73. h1 {
  74. color: #333;
  75. margin-top: 0;
  76. }
  77. .counter-display {
  78. text-align: center;
  79. padding: 30px;
  80. background: #f8f9fa;
  81. border-radius: 8px;
  82. margin: 20px 0;
  83. }
  84. .counter-value {
  85. font-size: 72px;
  86. font-weight: bold;
  87. color: #667eea;
  88. line-height: 1;
  89. }
  90. .counter-label {
  91. color: #666;
  92. font-size: 14px;
  93. text-transform: uppercase;
  94. letter-spacing: 2px;
  95. }
  96. .button-group {
  97. display: flex;
  98. gap: 10px;
  99. justify-content: center;
  100. margin: 20px 0;
  101. }
  102. button {
  103. padding: 12px 24px;
  104. border: none;
  105. border-radius: 6px;
  106. cursor: pointer;
  107. font-size: 16px;
  108. font-weight: 600;
  109. transition: all 0.2s;
  110. }
  111. button:hover {
  112. transform: translateY(-2px);
  113. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  114. }
  115. .btn-primary {
  116. background: #667eea;
  117. color: white;
  118. }
  119. .btn-primary:hover {
  120. background: #5a6fd6;
  121. }
  122. .btn-danger {
  123. background: #e74c3c;
  124. color: white;
  125. }
  126. .btn-danger:hover {
  127. background: #c0392b;
  128. }
  129. .btn-success {
  130. background: #2ecc71;
  131. color: white;
  132. }
  133. .btn-success:hover {
  134. background: #27ae60;
  135. }
  136. .info-grid {
  137. display: grid;
  138. grid-template-columns: repeat(2, 1fr);
  139. gap: 15px;
  140. margin: 20px 0;
  141. }
  142. .info-item {
  143. background: #f8f9fa;
  144. padding: 15px;
  145. border-radius: 6px;
  146. text-align: center;
  147. }
  148. .info-label {
  149. font-size: 12px;
  150. color: #666;
  151. text-transform: uppercase;
  152. letter-spacing: 1px;
  153. }
  154. .info-value {
  155. font-size: 18px;
  156. font-weight: 600;
  157. color: #333;
  158. margin-top: 5px;
  159. }
  160. .status-positive {
  161. color: #2ecc71 !important;
  162. }
  163. .status-negative {
  164. color: #e74c3c !important;
  165. }
  166. .status-zero {
  167. color: #666 !important;
  168. }
  169. code {
  170. background: #e8e8e8;
  171. padding: 2px 6px;
  172. border-radius: 4px;
  173. font-size: 14px;
  174. }
  175. pre {
  176. background: #263238;
  177. color: #aed581;
  178. padding: 15px;
  179. border-radius: 6px;
  180. overflow-x: auto;
  181. font-size: 13px;
  182. }
  183. .feature-list {
  184. margin: 0;
  185. padding-left: 20px;
  186. }
  187. .feature-list li {
  188. margin: 8px 0;
  189. color: #555;
  190. }
  191. a {
  192. color: #667eea;
  193. text-decoration: none;
  194. }
  195. a:hover {
  196. text-decoration: underline;
  197. }
  198. """;
  199. /**
  200. * InfoItemComponent - A single info grid item.
  201. */
  202. class InfoItemComponent : Component {
  203. public override string markup { get {
  204. return """
  205. <div class="info-item">
  206. <div class="info-label" sid="label"></div>
  207. <div class="info-value" sid="value"></div>
  208. </div>
  209. """;
  210. }}
  211. public string label { set {
  212. this["label"].text_content = value;
  213. }}
  214. public string info_value { set {
  215. this["value"].text_content = value;
  216. }}
  217. public string? status_class { set {
  218. var el = this["value"];
  219. if (value != null) {
  220. el.add_class(value);
  221. }
  222. }}
  223. }
  224. /**
  225. * InfoGridComponent - Container for info items.
  226. */
  227. class InfoGridComponent : Component {
  228. public void set_items(Enumerable<Renderable> items) {
  229. set_outlet_children("items", items);
  230. }
  231. public override string markup { get {
  232. return """
  233. <div class="info-grid">
  234. <spry-outlet sid="items"/>
  235. </div>
  236. """;
  237. }}
  238. }
  239. /**
  240. * CounterDisplayComponent - Shows the counter value with HTMX buttons.
  241. * Uses declarative spry-action and spry-target attributes.
  242. */
  243. class CounterDisplayComponent : Component {
  244. private ComponentFactory factory = inject<ComponentFactory>();
  245. private AppState app_state = inject<AppState>();
  246. public void set_info_items(Enumerable<Renderable> items) throws Error {
  247. var info_grid = factory.create<InfoGridComponent>();
  248. info_grid.set_items(items);
  249. set_outlet_child("info-grid", info_grid);
  250. }
  251. public override string markup { get {
  252. return """
  253. <div class="card" sid="counter-card">
  254. <div class="counter-display">
  255. <div class="counter-label">Current Value</div>
  256. <div class="counter-value" sid="counter-value">0</div>
  257. </div>
  258. <div class="button-group">
  259. <button type="button" class="btn-danger" sid="decrement-btn" spry-action=":Decrement" spry-target="counter-card" hx-swap="outerHTML">- Decrease</button>
  260. <button type="button" class="btn-primary" sid="reset-btn" spry-action=":Reset" spry-target="counter-card" hx-swap="outerHTML">Reset</button>
  261. <button type="button" class="btn-success" sid="increment-btn" spry-action=":Increment" spry-target="counter-card" hx-swap="outerHTML">+ Increase</button>
  262. </div>
  263. <spry-outlet sid="info-grid"/>
  264. </div>
  265. """;
  266. }}
  267. public async override void handle_action(string action) throws Error {
  268. // Update state based on action
  269. switch (action) {
  270. case "Increment":
  271. app_state.increment();
  272. break;
  273. case "Decrement":
  274. app_state.decrement();
  275. break;
  276. case "Reset":
  277. app_state.reset();
  278. break;
  279. }
  280. // Update counter value
  281. var counter_el = this["counter-value"];
  282. counter_el.text_content = app_state.counter.to_string();
  283. // Update status class
  284. counter_el.remove_class("status-positive");
  285. counter_el.remove_class("status-negative");
  286. counter_el.remove_class("status-zero");
  287. if (app_state.counter > 0) {
  288. counter_el.add_class("status-positive");
  289. } else if (app_state.counter < 0) {
  290. counter_el.add_class("status-negative");
  291. } else {
  292. counter_el.add_class("status-zero");
  293. }
  294. // Build info grid
  295. var info_grid = factory.create<InfoGridComponent>();
  296. var items = new Series<Renderable>();
  297. // Determine status
  298. string status_text;
  299. string status_class;
  300. if (app_state.counter > 0) {
  301. status_text = "Positive";
  302. status_class = "status-positive";
  303. } else if (app_state.counter < 0) {
  304. status_text = "Negative";
  305. status_class = "status-negative";
  306. } else {
  307. status_text = "Zero";
  308. status_class = "status-zero";
  309. }
  310. var status_item = factory.create<InfoItemComponent>();
  311. status_item.label = "Status";
  312. status_item.info_value = status_text;
  313. status_item.status_class = status_class;
  314. items.add(status_item);
  315. var action_item = factory.create<InfoItemComponent>();
  316. action_item.label = "Last Action";
  317. action_item.info_value = app_state.last_action;
  318. items.add(action_item);
  319. var time_item = factory.create<InfoItemComponent>();
  320. time_item.label = "Last Update";
  321. time_item.info_value = app_state.last_update.format("%H:%M:%S");
  322. items.add(time_item);
  323. var changes_item = factory.create<InfoItemComponent>();
  324. changes_item.label = "Total Changes";
  325. changes_item.info_value = app_state.total_changes.to_string();
  326. items.add(changes_item);
  327. info_grid.set_items(items);
  328. set_outlet_child("info-grid", info_grid);
  329. }
  330. public int counter { set {
  331. var counter_el = this["counter-value"];
  332. counter_el.text_content = value.to_string();
  333. // Update status class
  334. counter_el.remove_class("status-positive");
  335. counter_el.remove_class("status-negative");
  336. counter_el.remove_class("status-zero");
  337. if (value > 0) {
  338. counter_el.add_class("status-positive");
  339. } else if (value < 0) {
  340. counter_el.add_class("status-negative");
  341. } else {
  342. counter_el.add_class("status-zero");
  343. }
  344. }}
  345. }
  346. /**
  347. * CounterPageComponent - The main page structure.
  348. */
  349. class CounterPageComponent : Component {
  350. public void set_counter_section(Renderable component) {
  351. set_outlet_child("counter-section", component);
  352. }
  353. public override string markup { get {
  354. return """
  355. <!DOCTYPE html>
  356. <html lang="en">
  357. <head>
  358. <meta charset="UTF-8"/>
  359. <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  360. <title>Component Counter Example</title>
  361. <link rel="stylesheet" href="/styles.css"/>
  362. <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
  363. </head>
  364. <body>
  365. <div class="card">
  366. <h1>Component Counter Example</h1>
  367. <p>This page demonstrates using Spry.Component with IoC composition and HTMX.</p>
  368. </div>
  369. <spry-outlet sid="counter-section"/>
  370. <div class="card">
  371. <h2>How It Works</h2>
  372. <p>The counter uses declarative HTMX attributes for dynamic updates:</p>
  373. <pre>&lt;button spry-action=":Increment" spry-target="counter-card" hx-swap="outerHTML"&gt;+ Increase&lt;/button&gt;</pre>
  374. <h3>Component Features Used:</h3>
  375. <ul class="feature-list">
  376. <li><code>spry-outlet</code> - Placeholder for child components</li>
  377. <li><code>spry-action=":Action"</code> - Declarative HTMX action</li>
  378. <li><code>spry-target="sid"</code> - Target element by sid</li>
  379. <li><code>handle_action(action)</code> - Handle HTMX requests</li>
  380. <li><code>inject<ComponentFactory>()</code> - Factory for creating components</li>
  381. </ul>
  382. </div>
  383. <div class="card">
  384. <p style="text-align: center; color: #666; margin: 0;">
  385. Built with <code>Spry.Component</code> + HTMX |
  386. <a href="/raw">View Raw HTML</a>
  387. </p>
  388. </div>
  389. </body>
  390. </html>
  391. """;
  392. }}
  393. }
  394. /**
  395. * NoticeComponent - A simple notice for the raw view.
  396. */
  397. class NoticeComponent : Component {
  398. public override string markup { get {
  399. return """
  400. <div class="card" style="background: #fff3cd; color: #856404; border: 1px solid #ffc107;">
  401. <p>This is the raw template without dynamic modifications.</p>
  402. <p><a href="/">Back to dynamic version</a></p>
  403. </div>
  404. """;
  405. }}
  406. }
  407. // Home page endpoint - builds the component tree
  408. class HomePageEndpoint : Object, Endpoint {
  409. private AppState app_state = inject<AppState>();
  410. private ComponentFactory factory = inject<ComponentFactory>();
  411. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  412. // Determine status
  413. string status_text;
  414. string status_class;
  415. if (app_state.counter > 0) {
  416. status_text = "Positive";
  417. status_class = "status-positive";
  418. } else if (app_state.counter < 0) {
  419. status_text = "Negative";
  420. status_class = "status-negative";
  421. } else {
  422. status_text = "Zero";
  423. status_class = "status-zero";
  424. }
  425. // Create info items
  426. var items = new Series<Renderable>();
  427. var status_item = factory.create<InfoItemComponent>();
  428. status_item.label = "Status";
  429. status_item.info_value = status_text;
  430. status_item.status_class = status_class;
  431. items.add(status_item);
  432. var action_item = factory.create<InfoItemComponent>();
  433. action_item.label = "Last Action";
  434. action_item.info_value = app_state.last_action;
  435. items.add(action_item);
  436. var time_item = factory.create<InfoItemComponent>();
  437. time_item.label = "Last Update";
  438. time_item.info_value = app_state.last_update.format("%H:%M:%S");
  439. items.add(time_item);
  440. var changes_item = factory.create<InfoItemComponent>();
  441. changes_item.label = "Total Changes";
  442. changes_item.info_value = app_state.total_changes.to_string();
  443. items.add(changes_item);
  444. // Create counter display
  445. var counter_display = factory.create<CounterDisplayComponent>();
  446. counter_display.counter = app_state.counter;
  447. counter_display.set_info_items(items);
  448. // Create page and set counter section
  449. var page = factory.create<CounterPageComponent>();
  450. page.set_counter_section(counter_display);
  451. // to_result() handles all outlet replacement
  452. return yield page.to_result();
  453. }
  454. }
  455. // Raw HTML endpoint - shows the unmodified template
  456. class RawHtmlEndpoint : Object, Endpoint {
  457. private ComponentFactory factory = inject<ComponentFactory>();
  458. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  459. var page = factory.create<CounterPageComponent>();
  460. var notice = factory.create<NoticeComponent>();
  461. // Add a notice component
  462. page.set_counter_section(notice);
  463. return yield page.to_result();
  464. }
  465. }
  466. void main(string[] args) {
  467. int port = args.length > 1 ? int.parse(args[1]) : 8080;
  468. print("═══════════════════════════════════════════════════════════════\n");
  469. print(" Spry CounterComponent Example (IoC + HTMX)\n");
  470. print("═══════════════════════════════════════════════════════════════\n");
  471. print(" Port: %d\n", port);
  472. print("═══════════════════════════════════════════════════════════════\n");
  473. print(" Endpoints:\n");
  474. print(" / - Counter page (Component-based with HTMX)\n");
  475. print(" /styles.css - CSS stylesheet (FastResource)\n");
  476. print(" /raw - Raw template (no modifications)\n");
  477. print("═══════════════════════════════════════════════════════════════\n");
  478. print(" HTMX Actions (via SpryModule):\n");
  479. print(" Increment, Decrement, Reset - Dynamic updates\n");
  480. print("═══════════════════════════════════════════════════════════════\n");
  481. print("\nPress Ctrl+C to stop the server\n\n");
  482. try {
  483. var application = new WebApplication(port);
  484. // Register compression components
  485. application.use_compression();
  486. // Add Spry module for automatic HTMX endpoint registration
  487. application.add_module<SpryModule>();
  488. // Register application state as singleton
  489. application.add_singleton<AppState>();
  490. // Register ComponentFactory as scoped
  491. application.add_scoped<ComponentFactory>();
  492. // Register components as transient (created via factory)
  493. application.add_transient<InfoItemComponent>();
  494. application.add_transient<InfoGridComponent>();
  495. application.add_transient<CounterDisplayComponent>();
  496. application.add_transient<CounterPageComponent>();
  497. application.add_transient<NoticeComponent>();
  498. // Register CSS as a FastResource
  499. application.add_startup_endpoint<FastResource>(new EndpointRoute("/styles.css"), () => {
  500. try {
  501. return new FastResource.from_string(COUNTER_CSS)
  502. .with_content_type("text/css; charset=utf-8")
  503. .with_default_compressors();
  504. } catch (Error e) {
  505. error("Failed to create CSS resource: %s", e.message);
  506. }
  507. });
  508. // Register endpoints
  509. application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
  510. application.add_endpoint<RawHtmlEndpoint>(new EndpointRoute("/raw"));
  511. application.run();
  512. } catch (Error e) {
  513. printerr("Error: %s\n", e.message);
  514. Process.exit(1);
  515. }
  516. }