# 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}-{context_key}`. ### 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 """ """; }} // 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 |