ComponentsActionsPage.vala 15 KB

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