CounterComponent.vala 15 KB

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