using Spry; using Inversion; /** * ComponentsActionsPage - Documentation for Spry component actions * * This page covers how actions work, the spry-action attribute syntax, * and how to handle user interactions in components. */ public class ComponentsActionsPage : PageComponent { public const string ROUTE = "/components/actions"; private ComponentFactory factory = inject(); public override string markup { get { return """

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.

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.

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.

💡 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.

💡 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:

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:

Next Steps

"""; }} public override async void prepare() throws Error { // Same-component action examples var same_action_html = get_component_child("same-action-html"); same_action_html.language = "HTML"; same_action_html.code = "
\n" + " Off\n" + " \n" + "
"; var same_action_vala = get_component_child("same-action-vala"); same_action_vala.language = "Vala"; same_action_vala.code = "public async override void handle_action(string action) throws Error {\n" + " // Get item ID from hx-vals (passed via query params)\n" + " var id = get_id_from_query_params();\n\n" + " if (action == \"Toggle\") {\n" + " // Modify state in the STORE, not instance variables!\n" + " store.toggle(id);\n" + " }\n" + " // prepare() is called automatically to update template from store\n" + "}"; // Cross-component action examples var cross_action_html = get_component_child("cross-action-html"); cross_action_html.language = "HTML"; cross_action_html.code = "\n" + "
\n" + " \n" + " \n" + "
"; var cross_action_vala = get_component_child("cross-action-vala"); cross_action_vala.language = "Vala"; cross_action_vala.code = "// In TodoListComponent\n" + "public async override void handle_action(string action) throws Error {\n" + " if (action == \"Add\") {\n" + " var title = http_context.request.query_params.get_any_or_default(\"title\");\n" + " // Modify state in the store\n" + " store.add(title);\n" + " }\n" + " // prepare() rebuilds the list from store data\n" + "}"; // Target example var target_example = get_component_child("target-example"); target_example.language = "HTML"; target_example.code = "
\n" + " Item Title\n" + " \n" + "
"; // hx-vals examples var hx_vals_html = get_component_child("hx-vals-html"); hx_vals_html.language = "HTML"; hx_vals_html.code = "
\n" + " \n" + " \n" + " \n" + "
"; var hx_vals_vala = get_component_child("hx-vals-vala"); hx_vals_vala.language = "Vala"; hx_vals_vala.code = "public async override void handle_action(string action) throws Error {\n" + " // Get the id from query params (passed via hx-vals)\n" + " var id_str = http_context.request.query_params.get_any_or_default(\"id\");\n" + " var id = int.parse(id_str);\n\n" + " // Use the id to find and modify the item IN THE STORE\n" + " switch (action) {\n" + " case \"Toggle\":\n" + " store.toggle(id);\n" + " break;\n" + " case \"Delete\":\n" + " store.remove(id);\n" + " break;\n" + " }\n" + "}"; // Complete example with store var store_example = get_component_child("store-example"); store_example.language = "Vala"; store_example.code = "// First, define a store (singleton) to hold state\n" + "public class ToggleStore : Object {\n" + " private bool _is_on = false;\n" + " public bool is_on { get { return _is_on; } }\n\n" + " public void toggle() {\n" + " _is_on = !_is_on;\n" + " }\n" + "}\n\n" + "// Register as singleton in your app:\n" + "// application.add_singleton();\n\n" + "// Then use it in your component:\n" + "public class ToggleComponent : Component {\n" + " private ToggleStore store = inject(); // Inject singleton\n\n" + " public override string markup { get {\n" + " return \"\"\"\n" + "
\n" + " \n" + " \n" + "
\n" + " \"\"\";\n" + " }}\n\n" + " public override void prepare() throws Error {\n" + " // Read state from store\n" + " this[\"status\"].text_content = store.is_on ? \"ON\" : \"OFF\";\n" + " this[\"btn\"].text_content = store.is_on ? \"Turn Off\" : \"Turn On\";\n" + " }\n\n" + " public async override void handle_action(string action) throws Error {\n" + " if (action == \"Toggle\") {\n" + " // Modify state in the store - this persists!\n" + " store.toggle();\n" + " }\n" + " }\n" + "}"; // Set up the counter demo var demo = get_component_child("counter-demo"); demo.demo_component_name = "SimpleCounterDemo"; demo.source_file = "demo/DemoComponents/SimpleCounterDemo.vala"; } }