# 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 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 `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" } } ``` ## 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. ## 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 |