# Spry Framework - Important Details
## Component Basics
### Template DOM is NOT Persisted
- The template is fresh on every request
- Any state set via setters (like `title`, `completed`) is NOT available in `handle_action()`
- Always use `prepare()` to fetch data from store and set up the template
### The `prepare()` Method
- Called automatically before serialization
- Use this to fetch data from stores and populate the template
- Centralizes data fetching logic in one place
```vala
public override async void prepare() throws Error {
var item = store.get_by_id(_item_id);
if (item == null) return;
this["title"].text_content = item.title;
this["button"].text_content = item.completed ? "Undo" : "Done";
}
```
### 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
- For delete with `hx-swap="delete"`, just return (no content needed)
```vala
public async override void handle_action(string action) throws Error {
var id = get_id_from_query_params();
_item_id = id; // Set for prepare()
switch (action) {
case "Toggle":
store.toggle(id);
break; // prepare() will be called automatically
case "Delete":
store.remove(id);
return; // HTMX removes element via hx-swap="delete"
}
}
```
## 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 dynamic section updates to the client
yield continuation_context.send_dynamic("progress-bar");
yield continuation_context.send_dynamic("status");
Timeout.add(500, () => {
continuation.callback();
return false;
});
yield;
}
status = "Complete!";
yield continuation_context.send_dynamic("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_dynamic(name)` | Send a dynamic section (by `spry-dynamic` name) as an SSE event |
| `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 for its children:
```vala
public override string markup { get {
return """
0%
Status:Initializing...
""";
}}
```
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 `spry-dynamic` Attribute
Use `spry-dynamic="name"` on child elements to mark them as updatable sections:
```html
...
...
```
**Requirements:**
- Must be a child of an element with `spry-continuation`
- Automatically gets `sse-swap="_spry-dynamic-{name}"` and `hx-swap="outerHTML"`
When `continuation_context.send_dynamic("progress-bar")` is called:
1. The element with `spry-dynamic="progress-bar"` is located
2. Its HTML content is rendered and sent as an SSE event
3. HTMX swaps it into the matching element on the client
### The `spry-unique` Attribute
Use `spry-unique` to generate unique IDs for elements that need stable targeting:
```html
...
```
**Restrictions:**
- Cannot specify an `id` attribute on the same element
- Cannot be used inside `spry-per-*` loops (or children of loops)
The generated ID format is `_spry-unique-{counter}-{instance_id}`.
### Required Scripts for SSE
Include the HTMX SSE extension in your markup:
```html
```
## HTMX Integration
### Declarative Attributes
#### `spry-action` - Declare HTMX Actions
- `spry-action=":ActionName"` - Action on same component (e.g., `:Toggle`)
- `spry-action="ComponentName:ActionName"` - Action on different component (cross-component)
#### `spry-target` - Scoped Targeting (within same component)
- `spry-target="sid"` - Targets element by its `sid` attribute
- Only works within the SAME component
- Automatically generates proper `hx-target` with unique IDs
#### `hx-target` - Global Targeting (cross-component)
- Use `hx-target="#id"` to target elements anywhere on the page
- Requires the target element to have an `id` attribute (not just `sid`)
#### `hx-swap` - Swap Strategies
- `hx-swap="outerHTML"` - Replace entire target element (prevents nesting)
- `hx-swap="delete"` - Remove target element (no response content needed)
### Out-of-Band Swaps with `spry-global` and `add_globals_from()`
Update multiple elements on the page with a single response using `spry-global`:
```vala
// 1. Add spry-global to elements that may need OOB updates
// 2. Use prepare() to fetch data from store - no manual property setting needed!
class HeaderComponent : Component {
private TodoStore todo_store = inject();
public override string markup { get {
return """
Todo App
""";
}}
// prepare() fetches data from store automatically
public override async void prepare() throws Error {
this["total"].text_content = todo_store.count().to_string();
}
}
// 3. Inject the component and pass it to add_globals_from()
class ItemComponent : Component {
private HeaderComponent header = inject();
public override string markup { get {
return """
...
""";
}}
public async override void handle_action(string action) throws Error {
store.toggle(id);
// Add header globals for OOB swap (header's prepare() fetches stats)
add_globals_from(header);
}
}
```
The `spry-global="key"` attribute:
- Automatically adds `hx-swap-oob="true"` when the element appears in a response
- HTMX finds the existing element on the page and swaps it in place
- Use `add_globals_from(component)` to append spry-global elements from another component
- The component's `prepare()` method is called automatically to populate data
### Passing Data with `hx-vals`
Since template DOM is not persisted, use `hx-vals` to pass data:
```vala
// In prepare() - set on parent element, inherited by children
this["item"].set_attribute("hx-vals", @"{\"id\":$_item_id}");
// In handle_action()
var id_str = http_context.request.query_params.get_any_or_default("id");
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 """
0%
""";
}}
}
```
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
Item
```
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
Admin Panel
Moderator Panel
User Panel
```
### Loop Rendering with `spry-per-*`
Use `spry-per-{varname}="expression"` to iterate over collections:
```html
```
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
```
This resolves to `/_spry/res/{resource-name}` automatically.
## IoC Composition
### Registration Patterns
```vala
// State as singleton
application.add_singleton();
// Factory as scoped
application.add_scoped();
// Components as transient
application.add_transient();
```
### Dependency Injection
Use `inject()` in field initializers:
```vala
class MyComponent : Component {
private TodoStore store = inject();
private ComponentFactory factory = inject();
private HttpContext http_context = inject();
}
```
### Creating Components
Use `ComponentFactory.create()` (not `inject()` which only works in field initializers):
```vala
var child = factory.create();
child.item_id = item.id;
items.add(child);
```
## Common Patterns
### List with Items Pattern
```vala
// Parent component
class ListComponent : Component {
public void set_items(Enumerable items) {
set_outlet_children("items", items);
}
public override string markup { get {
return """
""";
}}
}
// Item component with prepare()
class ItemComponent : Component {
private TodoStore store = inject();
private HttpContext http_context = inject();
private HeaderComponent header = inject();
private int _item_id;
public int item_id { set { _item_id = value; } }
public override string markup { get {
return """
""";
}}
public override void prepare() throws Error {
var item = store.get_by_id(_item_id);
this["title"].text_content = item.title;
// hx-vals on parent is inherited by children
this["item"].set_attribute("hx-vals", @"{\"id\":$_item_id}");
}
public async override void handle_action(string action) throws Error {
var id = int.parse(http_context.request.query_params.get_any_or_default("id"));
_item_id = id;
if (action == "Toggle") {
store.toggle(id);
// Add header globals for OOB swap (header's prepare() fetches stats)
add_globals_from(header);
}
}
}
// Creating the list
var items = new Series();
store.all().iterate((item) => {
var component = factory.create();
component.item_id = item.id; // Only set ID, prepare() handles rest
items.add(component);
});
list.set_items(items);
```
### Cross-Component Action Pattern
```vala
// Form in one component triggers action on another
// ListComponent handles the Add action
class ListComponent : Component {
public async override void handle_action(string action) throws Error {
if (action == "Add") {
var title = http_context.request.query_params.get_any_or_default("title");
store.add(title);
}
// Rebuild list...
}
}
```
## HTML Escaping
`
` and `` tags do NOT escape their contents. Manually escape:
```vala
// Wrong
// Correct
<button spry-action=":Toggle">Click</button>
```
## SpryModule
Automatically registers `ComponentEndpoint` at `/_spry/{component-id}/{action}`:
```vala
application.add_module();
```
This enables the declarative `spry-action` attributes to work without manual endpoint registration.
## Declarative Child Components with ``
### When to Use `` vs ``
- **``** - For single, known child components
- **``** - For dynamic lists (multiple items from data)
### Declaring Child Components
Use `` in markup for declarative composition:
```vala
class TodoPage : PageComponent {
public override string markup { get {
return """
""";
}}
}
```
### Accessing Child Components with `get_component_child()`
Use `get_component_child(sid)` in `prepare()` to access and configure child components:
```vala
class TodoPage : PageComponent {
private TodoStore todo_store = inject();
private ComponentFactory factory = inject();
public override async void prepare() throws Error {
// Get child component and configure it
var todo_list = get_component_child("todo-list");
// Populate the list (which still uses spry-outlet for dynamic items)
var items = new Series();
todo_store.all().iterate((item) => {
var component = factory.create();
component.item_id = item.id;
items.add(component);
});
todo_list.set_items(items);
}
}
```
## PageComponent - Combining Component and Endpoint
`PageComponent` is a base class that acts as both a `Component` AND an `Endpoint`. This eliminates the need for separate endpoint classes:
```vala
// Before: Separate endpoint class
class HomePageEndpoint : Object, Endpoint {
private ComponentFactory factory = inject();
public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
var page = factory.create();
return yield page.to_result();
}
}
// After: PageComponent handles both roles
class TodoPage : PageComponent {
public override string markup { get { return "..."; } }
public override async void prepare() throws Error { /* ... */ }
}
```
## SpryConfigurator - Component and Page Registration
Use `SpryConfigurator` to register components and pages in a structured way:
```vala
// Get the configurator
var spry_cfg = application.configure_with();
// Register child components (transient lifecycle)
spry_cfg.add_component();
spry_cfg.add_component();
spry_cfg.add_component();
spry_cfg.add_component();
spry_cfg.add_component();
// Register pages (scoped lifecycle, acts as Endpoint)
spry_cfg.add_page(new EndpointRoute("/"));
// Other endpoints still use add_endpoint
application.add_endpoint(new EndpointRoute("/api/todos"));
```
### Registration Methods
| Method | Lifecycle | Use Case |
|--------|-----------|----------|
| `add_component()` | Transient | Child components created via factory |
| `add_page(route)` | Scoped | Page components that act as endpoints |
| `add_template(prefix)` | Transient | Page templates for layout wrapping |