| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- 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<ComponentFactory>();
-
- public override string markup { get {
- return """
- <div sid="page" class="doc-content">
- <h1>Actions</h1>
-
- <section class="doc-section">
- <p>
- 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.
- </p>
- </section>
-
- <section class="doc-section">
- <h2>How Actions Work</h2>
- <p>
- The flow of an action request:
- </p>
- <ol>
- <li>User interacts with an element that has <code>spry-action</code></li>
- <li>HTMX sends a request to the Spry action endpoint</li>
- <li>Spry creates a <strong>new</strong> component instance and calls <code>handle_action()</code></li>
- <li>Your component modifies state in stores (not instance variables!)</li>
- <li>Spry calls <code>prepare()</code> to update the template from store data</li>
- <li>The updated HTML is sent back and swapped into the page</li>
- </ol>
-
- <div class="warning-box">
- <p>
- <strong>⚠️ Critical:</strong> A <strong>new component instance</strong> is created for each action request.
- Any instance variables (like <code>is_on</code>, <code>count</code>) will NOT retain their values.
- Always store state in singleton stores, not component instance fields.
- </p>
- </div>
- </section>
-
- <section class="doc-section">
- <h2>The <code>spry-action</code> Attribute</h2>
- <p>
- The <code>spry-action</code> attribute specifies which action to trigger. There are
- two syntaxes depending on whether you're targeting the same component or a different one.
- </p>
-
- <h3>Same-Component Actions</h3>
- <p>
- Use <code>:ActionName</code> to trigger an action on the same component type that contains
- the element.
- </p>
-
- <spry-component name="CodeBlockComponent" sid="same-action-html"/>
- <spry-component name="CodeBlockComponent" sid="same-action-vala"/>
-
- <h3>Cross-Component Actions</h3>
- <p>
- Use <code>ComponentName:ActionName</code> to trigger an action on a different component type.
- This is useful when an interaction in one component should update another.
- </p>
-
- <spry-component name="CodeBlockComponent" sid="cross-action-html"/>
- <spry-component name="CodeBlockComponent" sid="cross-action-vala"/>
- </section>
-
- <section class="doc-section">
- <h2>The <code>spry-target</code> Attribute</h2>
- <p>
- The <code>spry-target</code> attribute specifies which element should be replaced
- when the action completes. It uses the <code>sid</code> of an element within the
- same component.
- </p>
-
- <spry-component name="CodeBlockComponent" sid="target-example"/>
-
- <div class="info-box">
- <p>
- <strong>💡 Tip:</strong> <code>spry-target</code> only works within the same component.
- For cross-component targeting, use <code>hx-target="#element-id"</code> with a
- global <code>id</code> attribute.
- </p>
- </div>
- </section>
-
- <section class="doc-section">
- <h2>Passing Data with <code>hx-vals</code></h2>
- <p>
- Since a new component instance is created for each request, you need to pass data
- explicitly using <code>hx-vals</code>. This data becomes available in query parameters,
- allowing you to identify which item to operate on.
- </p>
-
- <spry-component name="CodeBlockComponent" sid="hx-vals-html"/>
- <spry-component name="CodeBlockComponent" sid="hx-vals-vala"/>
-
- <div class="info-box">
- <p>
- <strong>💡 Tip:</strong> Set <code>hx-vals</code> on a parent element - it will be
- inherited by all child elements. This avoids repetition and keeps your code clean.
- </p>
- </div>
- </section>
-
- <section class="doc-section">
- <h2>Swap Strategies</h2>
- <p>
- Control how the response is inserted with <code>hx-swap</code>:
- </p>
-
- <table class="doc-table">
- <thead>
- <tr>
- <th>Value</th>
- <th>Description</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td><code>outerHTML</code></td>
- <td>Replace the entire target element (default for most cases)</td>
- </tr>
- <tr>
- <td><code>innerHTML</code></td>
- <td>Replace only the content inside the target</td>
- </tr>
- <tr>
- <td><code>delete</code></td>
- <td>Remove the target element (no response content needed)</td>
- </tr>
- <tr>
- <td><code>beforebegin</code></td>
- <td>Insert content before the target element</td>
- </tr>
- <tr>
- <td><code>afterend</code></td>
- <td>Insert content after the target element</td>
- </tr>
- </tbody>
- </table>
- </section>
-
- <section class="doc-section">
- <h2>Complete Example: Toggle with Store</h2>
- <p>
- Here's a complete example showing proper state management using a store.
- The store is a singleton that persists state across requests:
- </p>
-
- <spry-component name="CodeBlockComponent" sid="store-example"/>
- </section>
-
- <section class="doc-section">
- <h2>Live Demo: Counter</h2>
- <p>
- Try this interactive counter to see actions in action. The counter state
- is stored in a singleton store, so it persists between requests:
- </p>
-
- <spry-component name="DemoHostComponent" sid="counter-demo"/>
- </section>
-
- <section class="doc-section">
- <h2>Next Steps</h2>
- <div class="nav-cards">
- <a href="/components/outlets" class="nav-card">
- <h3>Outlets →</h3>
- <p>Compose components with outlets</p>
- </a>
- <a href="/components/continuations" class="nav-card">
- <h3>Continuations →</h3>
- <p>Real-time updates with SSE</p>
- </a>
- <a href="/components/template-syntax" class="nav-card">
- <h3>Template Syntax ←</h3>
- <p>Review template attributes</p>
- </a>
- </div>
- </section>
- </div>
- """;
- }}
-
- public override async void prepare() throws Error {
- // Same-component action examples
- var same_action_html = get_component_child<CodeBlockComponent>("same-action-html");
- same_action_html.language = "HTML";
- same_action_html.code = "<div sid=\"item\">\n" +
- " <span sid=\"status\">Off</span>\n" +
- " <button spry-action=\":Toggle\" spry-target=\"item\">Toggle</button>\n" +
- "</div>";
-
- var same_action_vala = get_component_child<CodeBlockComponent>("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<CodeBlockComponent>("cross-action-html");
- cross_action_html.language = "HTML";
- cross_action_html.code = "<!-- In AddFormComponent -->\n" +
- "<form spry-action=\"TodoList:Add\" hx-target=\"#todo-list\" hx-swap=\"outerHTML\">\n" +
- " <input name=\"title\" type=\"text\"/>\n" +
- " <button type=\"submit\">Add</button>\n" +
- "</form>";
-
- var cross_action_vala = get_component_child<CodeBlockComponent>("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<CodeBlockComponent>("target-example");
- target_example.language = "HTML";
- target_example.code = "<div sid=\"item\" class=\"item\">\n" +
- " <span sid=\"title\">Item Title</span>\n" +
- " <button spry-action=\":Delete\" spry-target=\"item\" hx-swap=\"delete\">\n" +
- " Delete\n" +
- " </button>\n" +
- "</div>";
-
- // hx-vals examples
- var hx_vals_html = get_component_child<CodeBlockComponent>("hx-vals-html");
- hx_vals_html.language = "HTML";
- hx_vals_html.code = "<div sid=\"item\" hx-vals='{\"id\": 42}'>\n" +
- " <!-- Children inherit hx-vals -->\n" +
- " <button spry-action=\":Toggle\" spry-target=\"item\">Toggle</button>\n" +
- " <button spry-action=\":Delete\" spry-target=\"item\">Delete</button>\n" +
- "</div>";
-
- var hx_vals_vala = get_component_child<CodeBlockComponent>("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<CodeBlockComponent>("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<ToggleStore>();\n\n" +
- "// Then use it in your component:\n" +
- "public class ToggleComponent : Component {\n" +
- " private ToggleStore store = inject<ToggleStore>(); // Inject singleton\n\n" +
- " public override string markup { get {\n" +
- " return \"\"\"\n" +
- " <div sid=\"toggle\" class=\"toggle-container\">\n" +
- " <span sid=\"status\" class=\"status\"></span>\n" +
- " <button sid=\"btn\" spry-action=\":Toggle\" spry-target=\"toggle\">\n" +
- " </button>\n" +
- " </div>\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<DemoHostComponent>("counter-demo");
- demo.demo_component_name = "SimpleCounterDemo";
- demo.source_file = "demo/DemoComponents/SimpleCounterDemo.vala";
- }
- }
|