ComponentsContinuationsPage.vala 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. using Spry;
  2. using Inversion;
  3. /**
  4. * ComponentsContinuationsPage - Documentation for Spry continuations (SSE)
  5. *
  6. * This page covers what continuations are, the spry-continuation attribute,
  7. * and how to send real-time updates to clients.
  8. */
  9. public class ComponentsContinuationsPage : PageComponent {
  10. public const string ROUTE = "/components/continuations";
  11. private ComponentFactory factory = inject<ComponentFactory>();
  12. public override string markup { get {
  13. return """
  14. <div sid="page" class="doc-content">
  15. <h1>Continuations</h1>
  16. <section class="doc-section">
  17. <p>
  18. Continuations enable real-time updates from server to client using Server-Sent Events (SSE).
  19. They're perfect for progress indicators, live status updates, and any scenario where
  20. the server needs to push updates to the client over time.
  21. </p>
  22. </section>
  23. <section class="doc-section">
  24. <h2>What are Continuations?</h2>
  25. <p>
  26. A continuation is a long-running server process that can send multiple updates
  27. to the client over a single HTTP connection. Unlike regular requests that return
  28. once, continuations can push updates as they happen.
  29. </p>
  30. <div class="info-box">
  31. <p>
  32. <strong>💡 Use Cases:</strong> Progress bars, build status, live logs,
  33. real-time notifications, file upload progress, long-running task monitoring.
  34. </p>
  35. </div>
  36. </section>
  37. <section class="doc-section">
  38. <h2>The <code>spry-continuation</code> Attribute</h2>
  39. <p>
  40. Add <code>spry-continuation</code> to an element to enable SSE for its children.
  41. This attribute is shorthand for:
  42. </p>
  43. <ul>
  44. <li><code>hx-ext="sse"</code> - Enable HTMX SSE extension</li>
  45. <li><code>sse-connect="(auto-endpoint)"</code> - Connect to SSE endpoint</li>
  46. <li><code>sse-close="_spry-close"</code> - Close event name</li>
  47. </ul>
  48. <spry-component name="CodeBlockComponent" sid="continuation-attr"/>
  49. </section>
  50. <section class="doc-section">
  51. <h2>The <code>spry-dynamic</code> Attribute</h2>
  52. <p>
  53. Mark child elements with <code>spry-dynamic="name"</code> to make them updatable.
  54. When you call <code>send_dynamic("name")</code>, that element's HTML is sent to
  55. the client and swapped in place.
  56. </p>
  57. <spry-component name="CodeBlockComponent" sid="dynamic-attr"/>
  58. </section>
  59. <section class="doc-section">
  60. <h2>The <code>continuation()</code> Method</h2>
  61. <p>
  62. Override <code>continuation(ContinuationContext context)</code> in your component
  63. to implement long-running processes that send updates.
  64. </p>
  65. <spry-component name="CodeBlockComponent" sid="continuation-method"/>
  66. </section>
  67. <section class="doc-section">
  68. <h2>ContinuationContext API</h2>
  69. <p>
  70. The <code>ContinuationContext</code> parameter provides methods for sending updates:
  71. </p>
  72. <table class="doc-table">
  73. <thead>
  74. <tr>
  75. <th>Method</th>
  76. <th>Description</th>
  77. </tr>
  78. </thead>
  79. <tbody>
  80. <tr>
  81. <td><code>send_dynamic(name)</code></td>
  82. <td>Send a dynamic section (by spry-dynamic name) as an SSE event</td>
  83. </tr>
  84. <tr>
  85. <td><code>send_json(event_type, node)</code></td>
  86. <td>Send JSON data as an SSE event</td>
  87. </tr>
  88. <tr>
  89. <td><code>send_string(event_type, data)</code></td>
  90. <td>Send raw string data as an SSE event</td>
  91. </tr>
  92. <tr>
  93. <td><code>send_full_update(event_type)</code></td>
  94. <td>Send the entire component document</td>
  95. </tr>
  96. <tr>
  97. <td><code>send_event(event)</code></td>
  98. <td>Send a custom SseEvent</td>
  99. </tr>
  100. </tbody>
  101. </table>
  102. </section>
  103. <section class="doc-section">
  104. <h2>Required Scripts</h2>
  105. <p>
  106. Include the HTMX SSE extension in your page for continuations to work:
  107. </p>
  108. <spry-component name="CodeBlockComponent" sid="scripts"/>
  109. </section>
  110. <section class="doc-section">
  111. <h2>The <code>spry-unique</code> Attribute</h2>
  112. <p>
  113. Use <code>spry-unique</code> on elements that need stable IDs for targeting.
  114. Spry generates unique IDs automatically.
  115. </p>
  116. <spry-component name="CodeBlockComponent" sid="unique-attr"/>
  117. <div class="warning-box">
  118. <p>
  119. <strong>⚠️ Restrictions:</strong> Cannot use <code>spry-unique</code> with an
  120. explicit <code>id</code> attribute, or inside <code>spry-per-*</code> loops.
  121. </p>
  122. </div>
  123. </section>
  124. <section class="doc-section">
  125. <h2>Cancellation Handling</h2>
  126. <p>
  127. Override <code>continuation_canceled()</code> to clean up resources when the
  128. client disconnects:
  129. </p>
  130. <spry-component name="CodeBlockComponent" sid="canceled-method"/>
  131. </section>
  132. <section class="doc-section">
  133. <h2>Live Demo: Progress Bar</h2>
  134. <p>
  135. This demo shows a progress bar that updates in real-time using SSE.
  136. Click "Start Task" to see continuations in action!
  137. </p>
  138. <spry-component name="DemoHostComponent" sid="progress-demo"/>
  139. </section>
  140. <section class="doc-section">
  141. <h2>Next Steps</h2>
  142. <div class="nav-cards">
  143. <a href="/page-components/overview" class="nav-card">
  144. <h3>Page Components →</h3>
  145. <p>Learn about page-level components</p>
  146. </a>
  147. <a href="/components/outlets" class="nav-card">
  148. <h3>Outlets ←</h3>
  149. <p>Component composition</p>
  150. </a>
  151. <a href="/components/actions" class="nav-card">
  152. <h3>Actions ←</h3>
  153. <p>Handle user interactions</p>
  154. </a>
  155. </div>
  156. </section>
  157. </div>
  158. """;
  159. }}
  160. public override async void prepare() throws Error {
  161. // spry-continuation attribute example
  162. var continuation_attr = get_component_child<CodeBlockComponent>("continuation-attr");
  163. continuation_attr.language = "HTML";
  164. continuation_attr.code = "<div spry-continuation>\n" +
  165. " <!-- Children can receive SSE updates -->\n" +
  166. " <div spry-dynamic=\"progress-bar\">\n" +
  167. " <div class=\"progress-bar\" style-width-expr='format(\"%i%%\", this.percent)'>\n" +
  168. " </div>\n" +
  169. " </div>\n" +
  170. " <div spry-dynamic=\"status\">\n" +
  171. " <strong>Status:</strong> <span spry-unique content-expr=\"this.status\">Ready</span>\n" +
  172. " </div>\n" +
  173. "</div>";
  174. // spry-dynamic attribute example
  175. var dynamic_attr = get_component_child<CodeBlockComponent>("dynamic-attr");
  176. dynamic_attr.language = "HTML";
  177. dynamic_attr.code = "<!-- spry-dynamic marks elements for SSE swapping -->\n" +
  178. "<div class=\"progress-container\" spry-dynamic=\"progress-bar\">\n" +
  179. " <div class=\"progress-bar\">...</div>\n" +
  180. "</div>\n\n" +
  181. "<div class=\"status\" spry-dynamic=\"status\">\n" +
  182. " <strong>Status:</strong> <span>Processing...</span>\n" +
  183. "</div>\n\n" +
  184. "<!-- Automatically gets: sse-swap=\"_spry-dynamic-{name}\" hx-swap=\"outerHTML\" -->";
  185. // continuation() method example
  186. var continuation_method = get_component_child<CodeBlockComponent>("continuation-method");
  187. continuation_method.language = "Vala";
  188. continuation_method.code = "public async override void continuation(ContinuationContext ctx) throws Error {\n" +
  189. " for (int i = 0; i <= 100; i += 10) {\n" +
  190. " percent = i;\n" +
  191. " status = @\"Processing... $(i)%\";\n\n" +
  192. " // Send dynamic section updates to the client\n" +
  193. " yield ctx.send_dynamic(\"progress-bar\");\n" +
  194. " yield ctx.send_dynamic(\"status\");\n\n" +
  195. " // Simulate async work\n" +
  196. " Timeout.add(500, () => {\n" +
  197. " continuation.callback();\n" +
  198. " return false;\n" +
  199. " });\n" +
  200. " yield;\n" +
  201. " }\n\n" +
  202. " status = \"Complete!\";\n" +
  203. " yield ctx.send_dynamic(\"status\");\n" +
  204. "}";
  205. // Required scripts example
  206. var scripts = get_component_child<CodeBlockComponent>("scripts");
  207. scripts.language = "HTML";
  208. scripts.code = "<!-- In your page template or component -->\n" +
  209. "<script spry-res=\"htmx.js\"></script>\n" +
  210. "<script spry-res=\"htmx-sse.js\"></script>";
  211. // spry-unique attribute example
  212. var unique_attr = get_component_child<CodeBlockComponent>("unique-attr");
  213. unique_attr.language = "HTML";
  214. unique_attr.code = "<!-- spry-unique generates a stable unique ID -->\n" +
  215. "<div class=\"progress-bar\" spry-unique>\n" +
  216. " <!-- Gets ID like: _spry-unique-0-abc123 -->\n" +
  217. "</div>\n\n" +
  218. "<!-- Use with content-expr for dynamic content -->\n" +
  219. "<span spry-unique content-expr=\"this.status\">Loading...</span>";
  220. // continuation_canceled example
  221. var canceled_method = get_component_child<CodeBlockComponent>("canceled-method");
  222. canceled_method.language = "Vala";
  223. canceled_method.code = "public async override void continuation_canceled() throws Error {\n" +
  224. " // Client disconnected - clean up resources\n" +
  225. " cancel_background_task();\n" +
  226. " release_file_handles();\n" +
  227. " log_message(\"Task canceled by client\");\n" +
  228. "}";
  229. // Set up the demo host
  230. var demo = get_component_child<DemoHostComponent>("progress-demo");
  231. demo.source_file = "demo/DemoComponents/ProgressDemo.vala";
  232. // Create and set the actual demo component
  233. var progress_demo = factory.create<ProgressDemoWithSSE>();
  234. demo.set_outlet_child("demo-outlet", progress_demo);
  235. }
  236. }