ComponentsContinuationsPage.vala 12 KB

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