|
|
@@ -13,7 +13,7 @@
|
|
|
- Centralizes data fetching logic in one place
|
|
|
|
|
|
```vala
|
|
|
-public override void prepare() throws Error {
|
|
|
+public override async void prepare() throws Error {
|
|
|
var item = store.get_by_id(_item_id);
|
|
|
if (item == null) return;
|
|
|
|
|
|
@@ -22,6 +22,18 @@ public override void prepare() throws Error {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
+### The `prepare_once()` Method
|
|
|
+- Called only once before the first `prepare()` call
|
|
|
+- Useful for one-time initialization that should not repeat on every render
|
|
|
+- Runs before `prepare()` in the same request lifecycle
|
|
|
+
|
|
|
+```vala
|
|
|
+public override async void prepare_once() throws Error {
|
|
|
+ // One-time setup, e.g., loading initial data
|
|
|
+ initial_data = yield fetch_initial_data();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
### The `handle_action()` Method
|
|
|
- Called when HTMX requests trigger an action
|
|
|
- Modify state in stores, then let `prepare()` handle template updates
|
|
|
@@ -43,6 +55,106 @@ public async override void handle_action(string action) throws Error {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
+## Real-Time Updates with Continuations (SSE)
|
|
|
+
|
|
|
+### Overview
|
|
|
+
|
|
|
+The continuation feature allows a Component to send real-time updates to the client via Server-Sent Events (SSE). This is useful for:
|
|
|
+- Long-running task progress reporting
|
|
|
+- Real-time status updates
|
|
|
+- Live data streaming
|
|
|
+
|
|
|
+### The `continuation()` Method
|
|
|
+
|
|
|
+Override `continuation(ContinuationContext context)` to send SSE events:
|
|
|
+
|
|
|
+```vala
|
|
|
+public async override void continuation(ContinuationContext continuation_context) throws Error {
|
|
|
+ for (int i = 0; i <= 100; i += 10) {
|
|
|
+ percent = i;
|
|
|
+ status = @"Processing... $(i)%";
|
|
|
+
|
|
|
+ // Send fragment updates to the client
|
|
|
+ yield continuation_context.send_fragment("progress", "progress-bar");
|
|
|
+ yield continuation_context.send_fragment("status", "status");
|
|
|
+
|
|
|
+ Timeout.add(500, () => {
|
|
|
+ continuation.callback();
|
|
|
+ return false;
|
|
|
+ });
|
|
|
+ yield;
|
|
|
+ }
|
|
|
+
|
|
|
+ status = "Complete!";
|
|
|
+ yield continuation_context.send_fragment("status", "status");
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### The `continuation_canceled()` Method
|
|
|
+
|
|
|
+Called when the client disconnects from the SSE stream:
|
|
|
+
|
|
|
+```vala
|
|
|
+public async override void continuation_canceled() throws Error {
|
|
|
+ // Clean up resources when client disconnects
|
|
|
+ cleanup_task();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### ContinuationContext API
|
|
|
+
|
|
|
+| Method | Description |
|
|
|
+|--------|-------------|
|
|
|
+| `send_fragment(event_type, sid)` | Send a fragment (by `sid`) as an SSE event with the given event type |
|
|
|
+| `send_json(event_type, node)` | Send JSON data as an SSE event |
|
|
|
+| `send_string(event_type, data)` | Send raw string data as an SSE event |
|
|
|
+| `send_full_update(event_type)` | Send the entire component document |
|
|
|
+| `send_event(event)` | Send a custom `SseEvent` |
|
|
|
+
|
|
|
+### The `spry-continuation` Attribute
|
|
|
+
|
|
|
+Add `spry-continuation` to an element to enable SSE:
|
|
|
+
|
|
|
+```vala
|
|
|
+public override string markup { get {
|
|
|
+ return """
|
|
|
+ <div spry-continuation>
|
|
|
+ <div class="progress-bar" sid="progress-bar" sse-swap="progress">
|
|
|
+ 0%
|
|
|
+ </div>
|
|
|
+ <div class="status" sid="status" sse-swap="status">
|
|
|
+ Initializing...
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ """;
|
|
|
+}}
|
|
|
+```
|
|
|
+
|
|
|
+The `spry-continuation` attribute is shorthand for:
|
|
|
+- `hx-ext="sse"` - Enable HTMX SSE extension
|
|
|
+- `sse-connect="(auto-generated-endpoint)"` - Connect to the SSE endpoint
|
|
|
+- `sse-close="_spry-close"` - Close event name
|
|
|
+
|
|
|
+### The `sse-swap` Attribute
|
|
|
+
|
|
|
+Use `sse-swap="eventname"` on child elements to specify which SSE event type should swap the content:
|
|
|
+
|
|
|
+```html
|
|
|
+<div sid="progress-bar" sse-swap="progress">...</div>
|
|
|
+<div sid="status" sse-swap="status">...</div>
|
|
|
+```
|
|
|
+
|
|
|
+When `continuation_context.send_fragment("progress", "progress-bar")` is called, the fragment with `sid="progress-bar"` is sent as an SSE event with type "progress", and HTMX swaps it into the element listening for that event type.
|
|
|
+
|
|
|
+### Required Scripts for SSE
|
|
|
+
|
|
|
+Include the HTMX SSE extension in your markup:
|
|
|
+
|
|
|
+```html
|
|
|
+<script spry-res="htmx.js"></script>
|
|
|
+<script spry-res="htmx-sse.js"></script>
|
|
|
+```
|
|
|
+
|
|
|
## HTMX Integration
|
|
|
|
|
|
### Declarative Attributes
|
|
|
@@ -130,6 +242,77 @@ var id = int.parse(id_str);
|
|
|
|
|
|
**Note**: `hx-vals` is inherited by child elements, so set it on the parent div rather than individual buttons.
|
|
|
|
|
|
+## Declarative Expression Attributes
|
|
|
+
|
|
|
+### Expression Attributes (`*-expr`)
|
|
|
+
|
|
|
+Use `*-expr` attributes to dynamically set any attribute based on component properties:
|
|
|
+
|
|
|
+```vala
|
|
|
+class ProgressComponent : Component {
|
|
|
+ public int percent { get; set; }
|
|
|
+
|
|
|
+ public override string markup { get {
|
|
|
+ return """
|
|
|
+ <div class="progress-bar"
|
|
|
+ content-expr='format("%i%%", this.percent)'
|
|
|
+ style-width-expr='format("%i%%", this.percent)'>
|
|
|
+ 0%
|
|
|
+ </div>
|
|
|
+ """;
|
|
|
+ }}
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Expression attribute patterns:
|
|
|
+- `content-expr="expression"` - Set text/HTML content
|
|
|
+- `class-expr="expression"` - Set CSS classes
|
|
|
+- `style-expr="expression"` - Set inline styles (e.g., `style-width-expr`)
|
|
|
+- `any-attr-expr="expression"` - Set any attribute dynamically
|
|
|
+
|
|
|
+### Conditional Class Expressions
|
|
|
+
|
|
|
+For `class-*` attributes, boolean expressions add/remove the class:
|
|
|
+
|
|
|
+```html
|
|
|
+<div class-completed-expr="this.is_completed">Item</div>
|
|
|
+```
|
|
|
+
|
|
|
+If `this.is_completed` is `true`, the `completed` class is added.
|
|
|
+
|
|
|
+### Conditional Rendering with `spry-if`
|
|
|
+
|
|
|
+Use `spry-if`, `spry-else-if`, and `spry-else` for conditional rendering:
|
|
|
+
|
|
|
+```html
|
|
|
+<div spry-if="this.is_admin">Admin Panel</div>
|
|
|
+<div spry-else-if="this.is_moderator">Moderator Panel</div>
|
|
|
+<div spry-else>User Panel</div>
|
|
|
+```
|
|
|
+
|
|
|
+### Loop Rendering with `spry-per-*`
|
|
|
+
|
|
|
+Use `spry-per-{varname}="expression"` to iterate over collections:
|
|
|
+
|
|
|
+```html
|
|
|
+<div spry-per-task="this.tasks">
|
|
|
+ <span content-expr="task.name"></span>
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+The variable name after `spry-per-` (e.g., `task`) becomes available in nested expressions.
|
|
|
+
|
|
|
+### Static Resources with `spry-res`
|
|
|
+
|
|
|
+Use `spry-res` to reference Spry's built-in static resources:
|
|
|
+
|
|
|
+```html
|
|
|
+<script spry-res="htmx.js"></script>
|
|
|
+<script spry-res="htmx-sse.js"></script>
|
|
|
+```
|
|
|
+
|
|
|
+This resolves to `/_spry/res/{resource-name}` automatically.
|
|
|
+
|
|
|
## IoC Composition
|
|
|
|
|
|
### Registration Patterns
|