ComponentsActionsPage.vala 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. using Spry;
  2. using Inversion;
  3. /**
  4. * ComponentsActionsPage - Documentation for Spry component actions
  5. *
  6. * This page covers how actions work, the spry-action attribute syntax,
  7. * and how to handle user interactions in components.
  8. */
  9. public class ComponentsActionsPage : PageComponent {
  10. public override string title { get { return "Actions - Spry"; } }
  11. public const string ROUTE = "/components/actions";
  12. private ComponentFactory factory = inject<ComponentFactory>();
  13. public override string markup { get {
  14. return """
  15. <div sid="page" class="doc-content">
  16. <h1>Actions</h1>
  17. <section class="doc-section">
  18. <p>
  19. Actions are the primary way to handle user interactions in Spry. When a user
  20. clicks a button, submits a form, or triggers any HTMX event, an action is sent
  21. to the server and handled by your component.
  22. </p>
  23. </section>
  24. <section class="doc-section">
  25. <h2>How Actions Work</h2>
  26. <p>
  27. The flow of an action request:
  28. </p>
  29. <ol>
  30. <li>User interacts with an element that has <code>spry-action</code></li>
  31. <li>HTMX sends a request to the Spry action endpoint</li>
  32. <li>Spry creates a <strong>new</strong> component instance and calls <code>handle_action()</code></li>
  33. <li>Your component modifies state in stores (not instance variables!)</li>
  34. <li>Spry calls <code>prepare()</code> to update the template from store data</li>
  35. <li>The updated HTML is sent back and swapped into the page</li>
  36. </ol>
  37. <div class="warning-box">
  38. <p>
  39. <strong>⚠️ Critical:</strong> A <strong>new component instance</strong> is created for each action request.
  40. Any instance variables (like <code>is_on</code>, <code>count</code>) will NOT retain their values.
  41. Always store state in singleton stores, not component instance fields.
  42. </p>
  43. </div>
  44. </section>
  45. <section class="doc-section">
  46. <h2>The <code>spry-action</code> Attribute</h2>
  47. <p>
  48. The <code>spry-action</code> attribute specifies which action to trigger. There are
  49. two syntaxes depending on whether you're targeting the same component or a different one.
  50. </p>
  51. <h3>Same-Component Actions</h3>
  52. <p>
  53. Use <code>:ActionName</code> to trigger an action on the same component type that contains
  54. the element.
  55. </p>
  56. <spry-component name="CodeBlockComponent" sid="same-action-html"/>
  57. <spry-component name="CodeBlockComponent" sid="same-action-vala"/>
  58. <h3>Cross-Component Actions</h3>
  59. <p>
  60. Use <code>ComponentName:ActionName</code> to trigger an action on a different component type.
  61. This is useful when an interaction in one component should update another.
  62. </p>
  63. <spry-component name="CodeBlockComponent" sid="cross-action-html"/>
  64. <spry-component name="CodeBlockComponent" sid="cross-action-vala"/>
  65. </section>
  66. <section class="doc-section">
  67. <h2>The <code>spry-target</code> Attribute</h2>
  68. <p>
  69. The <code>spry-target</code> attribute specifies which element should be replaced
  70. when the action completes. It uses the <code>sid</code> of an element within the
  71. same component.
  72. </p>
  73. <spry-component name="CodeBlockComponent" sid="target-example"/>
  74. <div class="info-box">
  75. <p>
  76. <strong>💡 Tip:</strong> <code>spry-target</code> only works within the same component.
  77. For cross-component targeting, use <code>hx-target="#element-id"</code> with a
  78. global <code>id</code> attribute.
  79. </p>
  80. </div>
  81. </section>
  82. <section class="doc-section">
  83. <h2>Passing Data with <code>hx-vals</code></h2>
  84. <p>
  85. Since a new component instance is created for each request, you need to pass data
  86. explicitly using <code>hx-vals</code>. This data becomes available in query parameters,
  87. allowing you to identify which item to operate on.
  88. </p>
  89. <spry-component name="CodeBlockComponent" sid="hx-vals-html"/>
  90. <spry-component name="CodeBlockComponent" sid="hx-vals-vala"/>
  91. <div class="info-box">
  92. <p>
  93. <strong>💡 Tip:</strong> Set <code>hx-vals</code> on a parent element - it will be
  94. inherited by all child elements. This avoids repetition and keeps your code clean.
  95. </p>
  96. </div>
  97. </section>
  98. <section class="doc-section">
  99. <h2>Swap Strategies</h2>
  100. <p>
  101. Control how the response is inserted with <code>hx-swap</code>:
  102. </p>
  103. <table class="doc-table">
  104. <thead>
  105. <tr>
  106. <th>Value</th>
  107. <th>Description</th>
  108. </tr>
  109. </thead>
  110. <tbody>
  111. <tr>
  112. <td><code>outerHTML</code></td>
  113. <td>Replace the entire target element (default for most cases)</td>
  114. </tr>
  115. <tr>
  116. <td><code>innerHTML</code></td>
  117. <td>Replace only the content inside the target</td>
  118. </tr>
  119. <tr>
  120. <td><code>delete</code></td>
  121. <td>Remove the target element (no response content needed)</td>
  122. </tr>
  123. <tr>
  124. <td><code>beforebegin</code></td>
  125. <td>Insert content before the target element</td>
  126. </tr>
  127. <tr>
  128. <td><code>afterend</code></td>
  129. <td>Insert content after the target element</td>
  130. </tr>
  131. </tbody>
  132. </table>
  133. </section>
  134. <section class="doc-section">
  135. <h2>Complete Example: Toggle with Store</h2>
  136. <p>
  137. Here's a complete example showing proper state management using a store.
  138. The store is a singleton that persists state across requests:
  139. </p>
  140. <spry-component name="CodeBlockComponent" sid="store-example"/>
  141. </section>
  142. <section class="doc-section">
  143. <h2>Live Demo: Counter</h2>
  144. <p>
  145. Try this interactive counter to see actions in action. The counter state
  146. is stored in a singleton store, so it persists between requests:
  147. </p>
  148. <spry-component name="DemoHostComponent" sid="counter-demo"/>
  149. </section>
  150. <section class="doc-section">
  151. <h2>Next Steps</h2>
  152. <div class="nav-cards">
  153. <a href="/components/outlets" class="nav-card">
  154. <h3>Outlets →</h3>
  155. <p>Compose components with outlets</p>
  156. </a>
  157. <a href="/components/continuations" class="nav-card">
  158. <h3>Continuations →</h3>
  159. <p>Real-time updates with SSE</p>
  160. </a>
  161. <a href="/components/template-syntax" class="nav-card">
  162. <h3>Template Syntax ←</h3>
  163. <p>Review template attributes</p>
  164. </a>
  165. </div>
  166. </section>
  167. </div>
  168. """;
  169. }}
  170. public override async void prepare() throws Error {
  171. // Same-component action examples
  172. var same_action_html = get_component_child<CodeBlockComponent>("same-action-html");
  173. same_action_html.language = "HTML";
  174. same_action_html.code = "<div sid=\"item\">\n" +
  175. " <span sid=\"status\">Off</span>\n" +
  176. " <button spry-action=\":Toggle\" spry-target=\"item\">Toggle</button>\n" +
  177. "</div>";
  178. var same_action_vala = get_component_child<CodeBlockComponent>("same-action-vala");
  179. same_action_vala.language = "Vala";
  180. same_action_vala.code = "public async override void handle_action(string action) throws Error {\n" +
  181. " // Get item ID from hx-vals (passed via query params)\n" +
  182. " var id = get_id_from_query_params();\n\n" +
  183. " if (action == \"Toggle\") {\n" +
  184. " // Modify state in the STORE, not instance variables!\n" +
  185. " store.toggle(id);\n" +
  186. " }\n" +
  187. " // prepare() is called automatically to update template from store\n" +
  188. "}";
  189. // Cross-component action examples
  190. var cross_action_html = get_component_child<CodeBlockComponent>("cross-action-html");
  191. cross_action_html.language = "HTML";
  192. cross_action_html.code = "<!-- In AddFormComponent -->\n" +
  193. "<form spry-action=\"TodoList:Add\" hx-target=\"#todo-list\" hx-swap=\"outerHTML\">\n" +
  194. " <input name=\"title\" type=\"text\"/>\n" +
  195. " <button type=\"submit\">Add</button>\n" +
  196. "</form>";
  197. var cross_action_vala = get_component_child<CodeBlockComponent>("cross-action-vala");
  198. cross_action_vala.language = "Vala";
  199. cross_action_vala.code = "// In TodoListComponent\n" +
  200. "public async override void handle_action(string action) throws Error {\n" +
  201. " if (action == \"Add\") {\n" +
  202. " var title = http_context.request.query_params.get_any_or_default(\"title\");\n" +
  203. " // Modify state in the store\n" +
  204. " store.add(title);\n" +
  205. " }\n" +
  206. " // prepare() rebuilds the list from store data\n" +
  207. "}";
  208. // Target example
  209. var target_example = get_component_child<CodeBlockComponent>("target-example");
  210. target_example.language = "HTML";
  211. target_example.code = "<div sid=\"item\" class=\"item\">\n" +
  212. " <span sid=\"title\">Item Title</span>\n" +
  213. " <button spry-action=\":Delete\" spry-target=\"item\" hx-swap=\"delete\">\n" +
  214. " Delete\n" +
  215. " </button>\n" +
  216. "</div>";
  217. // hx-vals examples
  218. var hx_vals_html = get_component_child<CodeBlockComponent>("hx-vals-html");
  219. hx_vals_html.language = "HTML";
  220. hx_vals_html.code = "<div sid=\"item\" hx-vals='{\"id\": 42}'>\n" +
  221. " <!-- Children inherit hx-vals -->\n" +
  222. " <button spry-action=\":Toggle\" spry-target=\"item\">Toggle</button>\n" +
  223. " <button spry-action=\":Delete\" spry-target=\"item\">Delete</button>\n" +
  224. "</div>";
  225. var hx_vals_vala = get_component_child<CodeBlockComponent>("hx-vals-vala");
  226. hx_vals_vala.language = "Vala";
  227. hx_vals_vala.code = "public async override void handle_action(string action) throws Error {\n" +
  228. " // Get the id from query params (passed via hx-vals)\n" +
  229. " var id_str = http_context.request.query_params.get_any_or_default(\"id\");\n" +
  230. " var id = int.parse(id_str);\n\n" +
  231. " // Use the id to find and modify the item IN THE STORE\n" +
  232. " switch (action) {\n" +
  233. " case \"Toggle\":\n" +
  234. " store.toggle(id);\n" +
  235. " break;\n" +
  236. " case \"Delete\":\n" +
  237. " store.remove(id);\n" +
  238. " break;\n" +
  239. " }\n" +
  240. "}";
  241. // Complete example with store
  242. var store_example = get_component_child<CodeBlockComponent>("store-example");
  243. store_example.language = "Vala";
  244. store_example.code = "// First, define a store (singleton) to hold state\n" +
  245. "public class ToggleStore : Object {\n" +
  246. " private bool _is_on = false;\n" +
  247. " public bool is_on { get { return _is_on; } }\n\n" +
  248. " public void toggle() {\n" +
  249. " _is_on = !_is_on;\n" +
  250. " }\n" +
  251. "}\n\n" +
  252. "// Register as singleton in your app:\n" +
  253. "// application.add_singleton<ToggleStore>();\n\n" +
  254. "// Then use it in your component:\n" +
  255. "public class ToggleComponent : Component {\n" +
  256. " private ToggleStore store = inject<ToggleStore>(); // Inject singleton\n\n" +
  257. " public override string markup { get {\n" +
  258. " return \"\"\"\n" +
  259. " <div sid=\"toggle\" class=\"toggle-container\">\n" +
  260. " <span sid=\"status\" class=\"status\"></span>\n" +
  261. " <button sid=\"btn\" spry-action=\":Toggle\" spry-target=\"toggle\">\n" +
  262. " </button>\n" +
  263. " </div>\n" +
  264. " \"\"\";\n" +
  265. " }}\n\n" +
  266. " public override void prepare() throws Error {\n" +
  267. " // Read state from store\n" +
  268. " this[\"status\"].text_content = store.is_on ? \"ON\" : \"OFF\";\n" +
  269. " this[\"btn\"].text_content = store.is_on ? \"Turn Off\" : \"Turn On\";\n" +
  270. " }\n\n" +
  271. " public async override void handle_action(string action) throws Error {\n" +
  272. " if (action == \"Toggle\") {\n" +
  273. " // Modify state in the store - this persists!\n" +
  274. " store.toggle();\n" +
  275. " }\n" +
  276. " }\n" +
  277. "}";
  278. // Set up the counter demo
  279. var demo = get_component_child<DemoHostComponent>("counter-demo");
  280. demo.demo_component_name = "SimpleCounterDemo";
  281. demo.source_file = "demo/DemoComponents/SimpleCounterDemo.vala";
  282. }
  283. }