| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- using Spry;
- using Inversion;
- /**
- * ComponentsContinuationsPage - Documentation for Spry continuations (SSE)
- *
- * This page covers what continuations are, the spry-continuation attribute,
- * and how to send real-time updates to clients.
- */
- public class ComponentsContinuationsPage : PageComponent {
-
- public const string ROUTE = "/components/continuations";
-
- private ComponentFactory factory = inject<ComponentFactory>();
-
- public override string markup { get {
- return """
- <div sid="page" class="doc-content">
- <h1>Continuations</h1>
-
- <section class="doc-section">
- <p>
- Continuations enable real-time updates from server to client using Server-Sent Events (SSE).
- They're perfect for progress indicators, live status updates, and any scenario where
- the server needs to push updates to the client over time.
- </p>
- </section>
-
- <section class="doc-section">
- <h2>What are Continuations?</h2>
- <p>
- A continuation is a long-running server process that can send multiple updates
- to the client over a single HTTP connection. Unlike regular requests that return
- once, continuations can push updates as they happen.
- </p>
-
- <div class="info-box">
- <p>
- <strong>💡 Use Cases:</strong> Progress bars, build status, live logs,
- real-time notifications, file upload progress, long-running task monitoring.
- </p>
- </div>
- </section>
-
- <section class="doc-section">
- <h2>The <code>spry-continuation</code> Attribute</h2>
- <p>
- Add <code>spry-continuation</code> to an element to enable SSE for its children.
- This attribute is shorthand for:
- </p>
- <ul>
- <li><code>hx-ext="sse"</code> - Enable HTMX SSE extension</li>
- <li><code>sse-connect="(auto-endpoint)"</code> - Connect to SSE endpoint</li>
- <li><code>sse-close="_spry-close"</code> - Close event name</li>
- </ul>
-
- <spry-component name="CodeBlockComponent" sid="continuation-attr"/>
- </section>
-
- <section class="doc-section">
- <h2>The <code>spry-dynamic</code> Attribute</h2>
- <p>
- Mark child elements with <code>spry-dynamic="name"</code> to make them updatable.
- When you call <code>send_dynamic("name")</code>, that element's HTML is sent to
- the client and swapped in place.
- </p>
-
- <spry-component name="CodeBlockComponent" sid="dynamic-attr"/>
- </section>
-
- <section class="doc-section">
- <h2>The <code>continuation()</code> Method</h2>
- <p>
- Override <code>continuation(ContinuationContext context)</code> in your component
- to implement long-running processes that send updates.
- </p>
-
- <spry-component name="CodeBlockComponent" sid="continuation-method"/>
- </section>
-
- <section class="doc-section">
- <h2>ContinuationContext API</h2>
- <p>
- The <code>ContinuationContext</code> parameter provides methods for sending updates:
- </p>
-
- <table class="doc-table">
- <thead>
- <tr>
- <th>Method</th>
- <th>Description</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td><code>send_dynamic(name)</code></td>
- <td>Send a dynamic section (by spry-dynamic name) as an SSE event</td>
- </tr>
- <tr>
- <td><code>send_json(event_type, node)</code></td>
- <td>Send JSON data as an SSE event</td>
- </tr>
- <tr>
- <td><code>send_string(event_type, data)</code></td>
- <td>Send raw string data as an SSE event</td>
- </tr>
- <tr>
- <td><code>send_full_update(event_type)</code></td>
- <td>Send the entire component document</td>
- </tr>
- <tr>
- <td><code>send_event(event)</code></td>
- <td>Send a custom SseEvent</td>
- </tr>
- </tbody>
- </table>
- </section>
-
- <section class="doc-section">
- <h2>Required Scripts</h2>
- <p>
- Include the HTMX SSE extension in your page for continuations to work:
- </p>
-
- <spry-component name="CodeBlockComponent" sid="scripts"/>
- </section>
-
- <section class="doc-section">
- <h2>The <code>spry-unique</code> Attribute</h2>
- <p>
- Use <code>spry-unique</code> on elements that need stable IDs for targeting.
- Spry generates unique IDs automatically.
- </p>
-
- <spry-component name="CodeBlockComponent" sid="unique-attr"/>
-
- <div class="warning-box">
- <p>
- <strong>⚠️ Restrictions:</strong> Cannot use <code>spry-unique</code> with an
- explicit <code>id</code> attribute, or inside <code>spry-per-*</code> loops.
- </p>
- </div>
- </section>
-
- <section class="doc-section">
- <h2>Cancellation Handling</h2>
- <p>
- Override <code>continuation_canceled()</code> to clean up resources when the
- client disconnects:
- </p>
-
- <spry-component name="CodeBlockComponent" sid="canceled-method"/>
- </section>
-
- <section class="doc-section">
- <h2>Live Demo: Progress Bar</h2>
- <p>
- This demo shows a progress bar that updates in real-time using SSE.
- Click "Start Task" to see continuations in action!
- </p>
-
- <spry-component name="DemoHostComponent" sid="progress-demo"/>
- </section>
-
- <section class="doc-section">
- <h2>Next Steps</h2>
- <div class="nav-cards">
- <a href="/page-components/overview" class="nav-card">
- <h3>Page Components →</h3>
- <p>Learn about page-level components</p>
- </a>
- <a href="/components/outlets" class="nav-card">
- <h3>Outlets ←</h3>
- <p>Component composition</p>
- </a>
- <a href="/components/actions" class="nav-card">
- <h3>Actions ←</h3>
- <p>Handle user interactions</p>
- </a>
- </div>
- </section>
- </div>
- """;
- }}
-
- public override async void prepare() throws Error {
- // spry-continuation attribute example
- var continuation_attr = get_component_child<CodeBlockComponent>("continuation-attr");
- continuation_attr.language = "HTML";
- continuation_attr.code = "<div spry-continuation>\n" +
- " <!-- Children can receive SSE updates -->\n" +
- " <div spry-dynamic=\"progress-bar\">\n" +
- " <div class=\"progress-bar\" style-width-expr='format(\"%i%%\", this.percent)'>\n" +
- " </div>\n" +
- " </div>\n" +
- " <div spry-dynamic=\"status\">\n" +
- " <strong>Status:</strong> <span spry-unique content-expr=\"this.status\">Ready</span>\n" +
- " </div>\n" +
- "</div>";
-
- // spry-dynamic attribute example
- var dynamic_attr = get_component_child<CodeBlockComponent>("dynamic-attr");
- dynamic_attr.language = "HTML";
- dynamic_attr.code = "<!-- spry-dynamic marks elements for SSE swapping -->\n" +
- "<div class=\"progress-container\" spry-dynamic=\"progress-bar\">\n" +
- " <div class=\"progress-bar\">...</div>\n" +
- "</div>\n\n" +
- "<div class=\"status\" spry-dynamic=\"status\">\n" +
- " <strong>Status:</strong> <span>Processing...</span>\n" +
- "</div>\n\n" +
- "<!-- Automatically gets: sse-swap=\"_spry-dynamic-{name}\" hx-swap=\"outerHTML\" -->";
-
- // continuation() method example
- var continuation_method = get_component_child<CodeBlockComponent>("continuation-method");
- continuation_method.language = "Vala";
- continuation_method.code = "public async override void continuation(ContinuationContext ctx) throws Error {\n" +
- " for (int i = 0; i <= 100; i += 10) {\n" +
- " percent = i;\n" +
- " status = @\"Processing... $(i)%\";\n\n" +
- " // Send dynamic section updates to the client\n" +
- " yield ctx.send_dynamic(\"progress-bar\");\n" +
- " yield ctx.send_dynamic(\"status\");\n\n" +
- " // Simulate async work\n" +
- " Timeout.add(500, () => {\n" +
- " continuation.callback();\n" +
- " return false;\n" +
- " });\n" +
- " yield;\n" +
- " }\n\n" +
- " status = \"Complete!\";\n" +
- " yield ctx.send_dynamic(\"status\");\n" +
- "}";
-
- // Required scripts example
- var scripts = get_component_child<CodeBlockComponent>("scripts");
- scripts.language = "HTML";
- scripts.code = "<!-- In your page template or component -->\n" +
- "<script spry-res=\"htmx.js\"></script>\n" +
- "<script spry-res=\"htmx-sse.js\"></script>";
-
- // spry-unique attribute example
- var unique_attr = get_component_child<CodeBlockComponent>("unique-attr");
- unique_attr.language = "HTML";
- unique_attr.code = "<!-- spry-unique generates a stable unique ID -->\n" +
- "<div class=\"progress-bar\" spry-unique>\n" +
- " <!-- Gets ID like: _spry-unique-0-abc123 -->\n" +
- "</div>\n\n" +
- "<!-- Use with content-expr for dynamic content -->\n" +
- "<span spry-unique content-expr=\"this.status\">Loading...</span>";
-
- // continuation_canceled example
- var canceled_method = get_component_child<CodeBlockComponent>("canceled-method");
- canceled_method.language = "Vala";
- canceled_method.code = "public async override void continuation_canceled() throws Error {\n" +
- " // Client disconnected - clean up resources\n" +
- " cancel_background_task();\n" +
- " release_file_handles();\n" +
- " log_message(\"Task canceled by client\");\n" +
- "}";
-
- // Set up the demo host
- var demo = get_component_child<DemoHostComponent>("progress-demo");
- demo.demo_component_name = "ProgressDemoWithSSE";
- demo.source_file = "demo/DemoComponents/ProgressDemo.vala";
- }
- }
|