ComponentsOutletsPage.vala 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. using Spry;
  2. using Inversion;
  3. /**
  4. * ComponentsOutletsPage - Documentation for Spry outlets
  5. *
  6. * This page covers what outlets are, how to use them for component
  7. * composition, and the parent/child component pattern.
  8. */
  9. public class ComponentsOutletsPage : PageComponent {
  10. public override string title { get { return "Outlets - Spry"; } }
  11. public const string ROUTE = "/components/outlets";
  12. private ComponentFactory factory = inject<ComponentFactory>();
  13. public override string markup { get {
  14. return """
  15. <div sid="page" class="doc-content">
  16. <h1>Outlets</h1>
  17. <section class="doc-section">
  18. <p>
  19. Outlets are placeholders in your component templates where child components
  20. can be dynamically inserted. They enable powerful component composition
  21. patterns, especially for lists and dynamic content.
  22. </p>
  23. </section>
  24. <section class="doc-section">
  25. <h2>What are Outlets?</h2>
  26. <p>
  27. An outlet is a special element in your markup that acts as a insertion point
  28. for child components. Think of it like a socket where you can plug in
  29. different components at runtime.
  30. </p>
  31. <spry-component name="CodeBlockComponent" sid="outlet-syntax"/>
  32. </section>
  33. <section class="doc-section">
  34. <h2>Using <code>set_outlet_children()</code></h2>
  35. <p>
  36. The <code>set_outlet_children(sid, children)</code> method populates an outlet
  37. with child components. Call it in your <code>prepare()</code> method after
  38. creating child components with the ComponentFactory.
  39. </p>
  40. <spry-component name="CodeBlockComponent" sid="set-outlet-vala"/>
  41. </section>
  42. <section class="doc-section">
  43. <h2>Parent/Child Component Pattern</h2>
  44. <p>
  45. The typical pattern for outlets involves:
  46. </p>
  47. <ol>
  48. <li>A <strong>parent component</strong> with an outlet and a method to set children</li>
  49. <li>A <strong>child component</strong> that displays individual items</li>
  50. <li>A <strong>store</strong> that holds the data</li>
  51. </ol>
  52. <h3>Parent Component</h3>
  53. <spry-component name="CodeBlockComponent" sid="parent-component"/>
  54. <h3>Child Component</h3>
  55. <spry-component name="CodeBlockComponent" sid="child-component"/>
  56. </section>
  57. <section class="doc-section">
  58. <h2>Creating Lists with Outlets</h2>
  59. <p>
  60. Outlets are perfect for rendering dynamic lists. Here's the complete pattern:
  61. </p>
  62. <spry-component name="CodeBlockComponent" sid="list-pattern"/>
  63. </section>
  64. <section class="doc-section">
  65. <h2>Outlets vs <code>&lt;spry-component&gt;</code></h2>
  66. <p>
  67. When should you use outlets vs declarative child components?
  68. </p>
  69. <table class="doc-table">
  70. <thead>
  71. <tr>
  72. <th>Feature</th>
  73. <th><code>&lt;spry-outlet&gt;</code></th>
  74. <th><code>&lt;spry-component&gt;</code></th>
  75. </tr>
  76. </thead>
  77. <tbody>
  78. <tr>
  79. <td>Use Case</td>
  80. <td>Dynamic lists, multiple items</td>
  81. <td>Single, known child components</td>
  82. </tr>
  83. <tr>
  84. <td>Population</td>
  85. <td><code>set_outlet_children()</code> in prepare()</td>
  86. <td>Automatic, configured via properties</td>
  87. </tr>
  88. <tr>
  89. <td>Access</td>
  90. <td>Not directly accessible</td>
  91. <td><code>get_component_child<T>()</code></td>
  92. </tr>
  93. <tr>
  94. <td>Examples</td>
  95. <td>Todo lists, data tables, feeds</td>
  96. <td>Headers, sidebars, fixed sections</td>
  97. </tr>
  98. </tbody>
  99. </table>
  100. </section>
  101. <section class="doc-section">
  102. <h2>Live Demo: Todo List</h2>
  103. <p>
  104. This interactive demo shows outlets in action. The todo list uses an outlet
  105. to render individual todo items. Try adding, toggling, and deleting items!
  106. </p>
  107. <spry-component name="DemoHostComponent" sid="todo-demo"/>
  108. </section>
  109. <section class="doc-section">
  110. <h2>Next Steps</h2>
  111. <div class="nav-cards">
  112. <a href="/components/continuations" class="nav-card">
  113. <h3>Continuations →</h3>
  114. <p>Real-time updates with SSE</p>
  115. </a>
  116. <a href="/components/actions" class="nav-card">
  117. <h3>Actions ←</h3>
  118. <p>Handle user interactions</p>
  119. </a>
  120. <a href="/components/template-syntax" class="nav-card">
  121. <h3>Template Syntax ←</h3>
  122. <p>Review template attributes</p>
  123. </a>
  124. </div>
  125. </section>
  126. </div>
  127. """;
  128. }}
  129. public override async void prepare() throws Error {
  130. // Outlet syntax example
  131. var outlet_syntax = get_component_child<CodeBlockComponent>("outlet-syntax");
  132. outlet_syntax.language = "HTML";
  133. outlet_syntax.code = "<div sid=\"list\">\n" +
  134. " <!-- This outlet will be filled with child components -->\n" +
  135. " <spry-outlet sid=\"items\"/>\n" +
  136. "</div>";
  137. // set_outlet_children example
  138. var set_outlet_vala = get_component_child<CodeBlockComponent>("set-outlet-vala");
  139. set_outlet_vala.language = "Vala";
  140. set_outlet_vala.code = "public override async void prepare() throws Error {\n" +
  141. " var factory = inject<ComponentFactory>();\n" +
  142. " var store = inject<TodoStore>();\n\n" +
  143. " // Create child components for each item\n" +
  144. " var children = new Series<Renderable>();\n" +
  145. " foreach (var item in store.items) {\n" +
  146. " var component = factory.create<TodoItemComponent>();\n" +
  147. " component.item_id = item.id; // Pass data to child\n" +
  148. " children.add(component);\n" +
  149. " }\n\n" +
  150. " // Populate the outlet\n" +
  151. " set_outlet_children(\"items\", children);\n" +
  152. "}";
  153. // Parent component example
  154. var parent_component = get_component_child<CodeBlockComponent>("parent-component");
  155. parent_component.language = "Vala";
  156. parent_component.code = "public class TodoListComponent : Component {\n" +
  157. " private TodoStore store = inject<TodoStore>();\n" +
  158. " private ComponentFactory factory = inject<ComponentFactory>();\n\n" +
  159. " public override string markup { get {\n" +
  160. " return \"\"\"\n" +
  161. " <div sid=\"list\" class=\"todo-list\">\n" +
  162. " <ul sid=\"items\">\n" +
  163. " <spry-outlet sid=\"items-outlet\"/>\n" +
  164. " </ul>\n" +
  165. " </div>\n" +
  166. " \"\"\";\n" +
  167. " }}\n\n" +
  168. " public override async void prepare() throws Error {\n" +
  169. " var children = new Series<Renderable>();\n" +
  170. " foreach (var item in store.items) {\n" +
  171. " var component = factory.create<TodoItemComponent>();\n" +
  172. " component.item_id = item.id;\n" +
  173. " children.add(component);\n" +
  174. " }\n" +
  175. " set_outlet_children(\"items-outlet\", children);\n" +
  176. " }\n" +
  177. "}";
  178. // Child component example
  179. var child_component = get_component_child<CodeBlockComponent>("child-component");
  180. child_component.language = "Vala";
  181. child_component.code = "public class TodoItemComponent : Component {\n" +
  182. " private TodoStore store = inject<TodoStore>();\n" +
  183. " private HttpContext http_context = inject<HttpContext>();\n\n" +
  184. " public int item_id { get; set; } // Set by parent\n\n" +
  185. " public override string markup { get {\n" +
  186. " return \"\"\"\n" +
  187. " <li sid=\"item\" class=\"todo-item\">\n" +
  188. " <span sid=\"title\"></span>\n" +
  189. " <button spry-action=\":Toggle\" spry-target=\"item\">Toggle</button>\n" +
  190. " </li>\n" +
  191. " \"\"\";\n" +
  192. " }}\n\n" +
  193. " public override void prepare() throws Error {\n" +
  194. " var item = store.get_by_id(item_id);\n" +
  195. " this[\"title\"].text_content = item.title;\n" +
  196. " // Pass item_id via hx-vals for handle_action\n" +
  197. " this[\"item\"].set_attribute(\"hx-vals\", @\"{\\\"id\\\":$item_id}\");\n" +
  198. " }\n\n" +
  199. " public async override void handle_action(string action) throws Error {\n" +
  200. " var id = int.parse(http_context.request.query_params.get_any_or_default(\"id\"));\n" +
  201. " if (action == \"Toggle\") {\n" +
  202. " store.toggle(id);\n" +
  203. " }\n" +
  204. " }\n" +
  205. "}";
  206. // List pattern example
  207. var list_pattern = get_component_child<CodeBlockComponent>("list-pattern");
  208. list_pattern.language = "Vala";
  209. list_pattern.code = "// 1. Define a store to hold data\n" +
  210. "public class TodoStore : Object {\n" +
  211. " public Series<TodoItem> items { get; default = new Series<TodoItem>(); }\n\n" +
  212. " public void add(string title) { /* ... */ }\n" +
  213. " public void toggle(int id) { /* ... */ }\n" +
  214. " public void remove(int id) { /* ... */ }\n" +
  215. "}\n\n" +
  216. "// 2. Register as singleton\n" +
  217. "application.add_singleton<TodoStore>();\n\n" +
  218. "// 3. Parent component uses outlet\n" +
  219. "public class TodoListComponent : Component {\n" +
  220. " private TodoStore store = inject<TodoStore>();\n\n" +
  221. " public override string markup { get {\n" +
  222. " return \"<ul><spry-outlet sid=\"items\"/></ul>\";\n" +
  223. " }}\n\n" +
  224. " public override async void prepare() throws Error {\n" +
  225. " var children = new Series<Renderable>();\n" +
  226. " foreach (var item in store.items) {\n" +
  227. " var child = factory.create<TodoItemComponent>();\n" +
  228. " child.item_id = item.id;\n" +
  229. " children.add(child);\n" +
  230. " }\n" +
  231. " set_outlet_children(\"items\", children);\n" +
  232. " }\n" +
  233. "}\n\n" +
  234. "// 4. Child component handles individual items\n" +
  235. "public class TodoItemComponent : Component {\n" +
  236. " public int item_id { get; set; }\n" +
  237. " // ... markup and methods\n" +
  238. "}";
  239. // Set up the demo host
  240. var demo = get_component_child<DemoHostComponent>("todo-demo");
  241. demo.demo_component_name = "TodoListDemo";
  242. demo.source_file = "demo/DemoComponents/TodoListDemo.vala";
  243. }
  244. }