Actions

Actions are the primary way to handle user interactions in Spry. When a user clicks a button, submits a form, or triggers any HTMX event, an action is sent to the server and handled by your component.

How Actions Work

The flow of an action request:

  1. User interacts with an element that has spry-action
  2. HTMX sends a request to the Spry action endpoint
  3. Spry creates a new component instance and calls handle_action()
  4. Your component modifies state in stores (not instance variables!)
  5. Spry calls prepare() to update the template from store data
  6. The updated HTML is sent back and swapped into the page

⚠️ Critical: A new component instance is created for each action request. Any instance variables (like is_on, count) will NOT retain their values. Always store state in singleton stores, not component instance fields.

The spry-action Attribute

The spry-action attribute specifies which action to trigger. There are two syntaxes depending on whether you're targeting the same component or a different one.

Same-Component Actions

Use :ActionName to trigger an action on the same component type that contains the element.

HTML
<div sid="item">
    <span sid="status">Off</span>
    <button spry-action=":Toggle" spry-target="item">Toggle</button>
</div>
Vala
public async override void handle_action(string action) throws Error {
    // Get item ID from hx-vals (passed via query params)
    var id = get_id_from_query_params();

    if (action == "Toggle") {
        // Modify state in the STORE, not instance variables!
        store.toggle(id);
    }
    // prepare() is called automatically to update template from store
}

Cross-Component Actions

Use ComponentName:ActionName to trigger an action on a different component type. This is useful when an interaction in one component should update another.

HTML
<!-- In AddFormComponent -->
<form spry-action="TodoList:Add" hx-target="#todo-list" hx-swap="outerHTML">
    <input name="title" type="text"/>
    <button type="submit">Add</button>
</form>
Vala
// In TodoListComponent
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");
        // Modify state in the store
        store.add(title);
    }
    // prepare() rebuilds the list from store data
}

The spry-target Attribute

The spry-target attribute specifies which element should be replaced when the action completes. It uses the sid of an element within the same component.

HTML
<div sid="item" class="item">
    <span sid="title">Item Title</span>
    <button spry-action=":Delete" spry-target="item" hx-swap="delete">
        Delete
    </button>
</div>

💡 Tip: spry-target only works within the same component. For cross-component targeting, use hx-target="#element-id" with a global id attribute.

Passing Data with hx-vals

Since a new component instance is created for each request, you need to pass data explicitly using hx-vals. This data becomes available in query parameters, allowing you to identify which item to operate on.

HTML
<div sid="item" hx-vals='{"id": 42}'>
    <!-- Children inherit hx-vals -->
    <button spry-action=":Toggle" spry-target="item">Toggle</button>
    <button spry-action=":Delete" spry-target="item">Delete</button>
</div>
Vala
public async override void handle_action(string action) throws Error {
    // Get the id from query params (passed via hx-vals)
    var id_str = http_context.request.query_params.get_any_or_default("id");
    var id = int.parse(id_str);

    // Use the id to find and modify the item IN THE STORE
    switch (action) {
        case "Toggle":
            store.toggle(id);
            break;
        case "Delete":
            store.remove(id);
            break;
    }
}

💡 Tip: Set hx-vals on a parent element - it will be inherited by all child elements. This avoids repetition and keeps your code clean.

Swap Strategies

Control how the response is inserted with hx-swap:

Value Description
outerHTML Replace the entire target element (default for most cases)
innerHTML Replace only the content inside the target
delete Remove the target element (no response content needed)
beforebegin Insert content before the target element
afterend Insert content after the target element

Complete Example: Toggle with Store

Here's a complete example showing proper state management using a store. The store is a singleton that persists state across requests:

Vala
// First, define a store (singleton) to hold state
public class ToggleStore : Object {
    private bool _is_on = false;
    public bool is_on { get { return _is_on; } }

    public void toggle() {
        _is_on = !_is_on;
    }
}

// Register as singleton in your app:
// application.add_singleton<ToggleStore>();

// Then use it in your component:
public class ToggleComponent : Component {
    private ToggleStore store = inject<ToggleStore>();  // Inject singleton

    public override string markup { get {
        return """
        <div sid="toggle" class="toggle-container">
            <span sid="status" class="status"></span>
            <button sid="btn" spry-action=":Toggle" spry-target="toggle">
            </button>
        </div>
        """;
    }}

    public override void prepare() throws Error {
        // Read state from store
        this["status"].text_content = store.is_on ? "ON" : "OFF";
        this["btn"].text_content = store.is_on ? "Turn Off" : "Turn On";
    }

    public async override void handle_action(string action) throws Error {
        if (action == "Toggle") {
            // Modify state in the store - this persists!
            store.toggle();
        }
    }
}

Live Demo: Counter

Try this interactive counter to see actions in action. The counter state is stored in a singleton store, so it persists between requests:

SimpleCounterDemo
3

Next Steps