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:
- User interacts with an element that has
spry-action - HTMX sends a request to the Spry action endpoint
- Spry creates a new component instance and calls
handle_action() - Your component modifies state in stores (not instance variables!)
- Spry calls
prepare()to update the template from store data - 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.
<div sid="item">
<span sid="status">Off</span>
<button spry-action=":Toggle" spry-target="item">Toggle</button>
</div>
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.
<!-- 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>
// 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.
<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.
<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>
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:
// 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: