ComponentsTemplateSyntaxPage.vala 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. using Spry;
  2. /**
  3. * ComponentsTemplateSyntaxPage - Documentation for Spry template syntax
  4. *
  5. * This page covers all the special HTML attributes and elements used in
  6. * Spry component templates.
  7. */
  8. public class ComponentsTemplateSyntaxPage : PageComponent {
  9. public override string title { get { return "Template Syntax - Spry"; } }
  10. public const string ROUTE = "/components/template-syntax";
  11. public override string markup { get {
  12. return """
  13. <div sid="page" class="doc-content">
  14. <h1>Template Syntax</h1>
  15. <section class="doc-section">
  16. <p>
  17. Spry extends HTML with special attributes that enable reactive behavior,
  18. component composition, and dynamic content. This page covers all the
  19. template syntax available in Spry components.
  20. </p>
  21. </section>
  22. <section class="doc-section">
  23. <h2>Special Attributes</h2>
  24. <h3><code>sid</code> - Spry ID</h3>
  25. <p>
  26. The <code>sid</code> attribute assigns a unique identifier to an element within
  27. a component. Use it to select and manipulate elements in your Vala code.
  28. </p>
  29. <spry-component name="CodeBlockComponent" sid="sid-html"/>
  30. <spry-component name="CodeBlockComponent" sid="sid-vala"/>
  31. </section>
  32. <section class="doc-section">
  33. <h3><code>spry-action</code> - HTMX Action Binding</h3>
  34. <p>
  35. The <code>spry-action</code> attribute binds an element to trigger a component action
  36. when interacted with. The action is sent to the server via HTMX.
  37. </p>
  38. <spry-component name="CodeBlockComponent" sid="action-example"/>
  39. <p>
  40. See the <a href="/components/actions">Actions</a> page for more details.
  41. </p>
  42. </section>
  43. <section class="doc-section">
  44. <h3><code>spry-target</code> - Scoped Targeting</h3>
  45. <p>
  46. The <code>spry-target</code> attribute specifies which element should be replaced
  47. when an action completes. It targets elements by their <code>sid</code> within the
  48. same component.
  49. </p>
  50. <spry-component name="CodeBlockComponent" sid="target-example"/>
  51. </section>
  52. <section class="doc-section">
  53. <h3><code>spry-global</code> - Out-of-Band Swaps</h3>
  54. <p>
  55. The <code>spry-global</code> attribute marks an element for out-of-band updates.
  56. When included in a response, HTMX will swap this element anywhere on the page
  57. that has a matching <code>id</code>.
  58. </p>
  59. <spry-component name="CodeBlockComponent" sid="global-html"/>
  60. <spry-component name="CodeBlockComponent" sid="global-vala"/>
  61. </section>
  62. <section class="doc-section">
  63. <h3><code>*-expr</code> - Expression Attributes</h3>
  64. <p>
  65. Expression attributes dynamically set HTML attributes based on component properties.
  66. Use <code>content-expr</code> for text content, <code>class-*-expr</code> for
  67. conditional classes, and <code>any-attr-expr</code> for any other attribute.
  68. </p>
  69. <spry-component name="CodeBlockComponent" sid="expr-example"/>
  70. </section>
  71. <section class="doc-section">
  72. <h3><code>spry-if</code> / <code>spry-else-if</code> / <code>spry-else</code> - Conditionals</h3>
  73. <p>
  74. Use these attributes for conditional rendering. Only elements with truthy
  75. conditions will be included in the output.
  76. </p>
  77. <spry-component name="CodeBlockComponent" sid="conditional-example"/>
  78. </section>
  79. <section class="doc-section">
  80. <h3><code>spry-per-*</code> - Loop Rendering</h3>
  81. <p>
  82. Use <code>spry-per-{varname}="expression"</code> to iterate over collections.
  83. The variable name becomes available in nested expressions.
  84. </p>
  85. <spry-component name="CodeBlockComponent" sid="loop-example"/>
  86. </section>
  87. <section class="doc-section">
  88. <h2>Special Elements</h2>
  89. <h3><code>&lt;spry-outlet&gt;</code> - Child Component Outlets</h3>
  90. <p>
  91. Outlets are placeholders where child components can be inserted dynamically.
  92. Use <code>set_outlet_children()</code> to populate outlets.
  93. </p>
  94. <spry-component name="CodeBlockComponent" sid="outlet-example"/>
  95. <p>
  96. See the <a href="/components/outlets">Outlets</a> page for more details.
  97. </p>
  98. </section>
  99. <section class="doc-section">
  100. <h3><code>&lt;spry-component&gt;</code> - Declarative Child Components</h3>
  101. <p>
  102. Use <code>&lt;spry-component&gt;</code> to declare child components directly in
  103. your template. Access them with <code>get_component_child&lt;T&gt;()</code>.
  104. </p>
  105. <spry-component name="CodeBlockComponent" sid="component-html"/>
  106. <spry-component name="CodeBlockComponent" sid="component-vala"/>
  107. </section>
  108. <section class="doc-section">
  109. <h3><code>&lt;spry-content&gt;</code> - Parent-Specified Content</h3>
  110. <p>
  111. The <code>&lt;spry-content&gt;</code> tag allows child components to define insertion points
  112. where parent components can inject content. When a parent uses <code>&lt;spry-component&gt;</code>
  113. with nested elements, those elements replace the <code>&lt;spry-content/&gt;</code> tag in the
  114. child component's rendered output.
  115. </p>
  116. <spry-component name="CodeBlockComponent" sid="content-example"/>
  117. <h4>Use Cases</h4>
  118. <p>
  119. This pattern is ideal for wrapper components that provide structure while allowing
  120. flexible content:
  121. </p>
  122. <ul>
  123. <li><strong>Cards</strong> - Wrap content in styled containers with headers/footers</li>
  124. <li><strong>Modals</strong> - Provide overlay structure with custom content</li>
  125. <li><strong>Alerts</strong> - Style messages with consistent formatting</li>
  126. <li><strong>Panels</strong> - Create collapsible or tabbed sections</li>
  127. </ul>
  128. </section>
  129. <section class="doc-section">
  130. <h3><code>&lt;spry-context&gt;</code> - Context Property Preservation</h3>
  131. <p>
  132. The <code>&lt;spry-context&gt;</code> tag marks a property to be preserved across
  133. action requests. When a component action is triggered, properties marked with this
  134. tag are encrypted and included in the action URL, then restored when the action
  135. is handled.
  136. </p>
  137. <spry-component name="CodeBlockComponent" sid="context-example"/>
  138. <h4>How It Works</h4>
  139. <ul>
  140. <li>Properties marked with <code>&lt;spry-context property="name"/&gt;</code> are tracked</li>
  141. <li>When an action is triggered, these properties are encrypted using <code>CryptographyProvider</code></li>
  142. <li>The encrypted context is included in the action URL</li>
  143. <li>The server decrypts and restores the properties before calling <code>handle_action()</code></li>
  144. </ul>
  145. <div class="warning-box">
  146. <h4>⚠️ Security Warning: Replay Attacks</h4>
  147. <p>
  148. The encrypted context can be captured and replayed by malicious actors. While the
  149. data cannot be tampered with (it's cryptographically signed), an attacker could
  150. capture a valid request and replay it later.
  151. </p>
  152. <ul>
  153. <li><strong>Do not</strong> use context for sensitive data that could be exploited if replayed</li>
  154. <li><strong>Do not</strong> use context for authentication or authorization decisions</li>
  155. <li><strong>Consider</strong> adding timestamps or expiration for time-sensitive operations</li>
  156. </ul>
  157. <p>
  158. <strong>Example attack:</strong> If context contains admin privileges, an attacker
  159. could capture a request from an admin session and replay it to gain unauthorized access.
  160. </p>
  161. </div>
  162. <h4>Best Practices</h4>
  163. <ul>
  164. <li>Use context for non-sensitive data like IDs, flags, or UI configuration</li>
  165. <li>Validate context data on the server before using it</li>
  166. <li>Keep context data minimal - only include what's needed</li>
  167. </ul>
  168. </section>
  169. <section class="doc-section">
  170. <h3><code>spry-res</code> - Static Resources</h3>
  171. <p>
  172. Use <code>spry-res</code> to reference Spry's built-in static resources.
  173. This resolves to <code>/_spry/res/{resource-name}</code> automatically.
  174. </p>
  175. <spry-component name="CodeBlockComponent" sid="res-example"/>
  176. </section>
  177. <section class="doc-section">
  178. <h2>Next Steps</h2>
  179. <div class="nav-cards">
  180. <a href="/components/actions" class="nav-card">
  181. <h3>Actions →</h3>
  182. <p>Handle user interactions with actions</p>
  183. </a>
  184. <a href="/components/outlets" class="nav-card">
  185. <h3>Outlets →</h3>
  186. <p>Compose components with outlets</p>
  187. </a>
  188. <a href="/components/continuations" class="nav-card">
  189. <h3>Continuations →</h3>
  190. <p>Real-time updates with SSE</p>
  191. </a>
  192. </div>
  193. </section>
  194. </div>
  195. """;
  196. }}
  197. public override async void prepare() throws Error {
  198. // sid examples
  199. var sid_html = get_component_child<CodeBlockComponent>("sid-html");
  200. sid_html.language = "HTML";
  201. sid_html.code = """<div sid="container">
  202. <span sid="title">Hello</span>
  203. <button sid="submit-btn">Submit</button>
  204. </div>""";
  205. var sid_vala = get_component_child<CodeBlockComponent>("sid-vala");
  206. sid_vala.language = "Vala";
  207. sid_vala.code = """// Access elements by sid in prepare() or handle_action()
  208. this["title"].text_content = "Updated Title";
  209. this["submit-btn"].set_attribute("disabled", "true");""";
  210. // spry-action example
  211. var action_example = get_component_child<CodeBlockComponent>("action-example");
  212. action_example.language = "HTML";
  213. action_example.code = """<!-- Same-component action -->
  214. <button spry-action=":Toggle">Toggle</button>
  215. <!-- Cross-component action -->
  216. <button spry-action="ListComponent:Add">Add Item</button>""";
  217. // spry-target example
  218. var target_example = get_component_child<CodeBlockComponent>("target-example");
  219. target_example.language = "HTML";
  220. target_example.code = """<div sid="item" class="item">
  221. <span sid="title">Item Title</span>
  222. <button spry-action=":Delete" spry-target="item">Delete</button>
  223. </div>""";
  224. // spry-global examples
  225. var global_html = get_component_child<CodeBlockComponent>("global-html");
  226. global_html.language = "HTML";
  227. global_html.code = """<!-- In HeaderComponent -->
  228. <div id="header" spry-global="header">
  229. <h1>Todo App</h1>
  230. <span sid="count">0 items</span>
  231. </div>""";
  232. var global_vala = get_component_child<CodeBlockComponent>("global-vala");
  233. global_vala.language = "Vala";
  234. global_vala.code = """// In another component's handle_action()
  235. add_globals_from(header); // Includes header in response for OOB swap""";
  236. // Expression attributes example
  237. var expr_example = get_component_child<CodeBlockComponent>("expr-example");
  238. expr_example.language = "HTML";
  239. expr_example.code = """<!-- Content expression -->
  240. <span content-expr='format("{{this.percent}}%")'>0%</span>
  241. <!-- Style expression -->
  242. <div style-width-expr='format("{{this.percent}}%")'></div>
  243. <!-- Conditional class -->
  244. <div class-completed-expr="this.is_completed">Task</div>
  245. <!-- Any attribute expression -->
  246. <input disabled-expr="this.is_readonly">""";
  247. // Conditional example
  248. var conditional_example = get_component_child<CodeBlockComponent>("conditional-example");
  249. conditional_example.language = "HTML";
  250. conditional_example.code = """<div spry-if="this.is_admin">Admin Panel</div>
  251. <div spry-else-if="this.is_moderator">Moderator Panel</div>
  252. <div spry-else>User Panel</div>""";
  253. // Loop example
  254. var loop_example = get_component_child<CodeBlockComponent>("loop-example");
  255. loop_example.language = "HTML";
  256. loop_example.code = """<ul>
  257. <li spry-per-task="this.tasks">
  258. <span content-expr="task.name"></span>
  259. <span content-expr="task.status"></span>
  260. </li>
  261. </ul>""";
  262. // Outlet example
  263. var outlet_example = get_component_child<CodeBlockComponent>("outlet-example");
  264. outlet_example.language = "HTML";
  265. outlet_example.code = """<div sid="list">
  266. <spry-outlet sid="items"/>
  267. </div>""";
  268. // Component examples
  269. var component_html = get_component_child<CodeBlockComponent>("component-html");
  270. component_html.language = "HTML";
  271. component_html.code = """<div>
  272. <spry-component name="HeaderComponent" sid="header"/>
  273. <spry-component name="TodoListComponent" sid="todo-list"/>
  274. <spry-component name="FooterComponent" sid="footer"/>
  275. </div>""";
  276. var component_vala = get_component_child<CodeBlockComponent>("component-vala");
  277. component_vala.language = "Vala";
  278. component_vala.code = """public override async void prepare() throws Error {
  279. var header = get_component_child<HeaderComponent>("header");
  280. header.title = "My App";
  281. }""";
  282. // Content example
  283. var content_example = get_component_child<CodeBlockComponent>("content-example");
  284. content_example.language = "HTML";
  285. content_example.code = """<!-- CardComponent template - defines where content goes -->
  286. <div class="card">
  287. <h2>Card Title</h2>
  288. <div class="card-body">
  289. <spry-content/> <!-- Parent's nested content appears here -->
  290. </div>
  291. </div>
  292. <!-- Parent component using CardComponent with injected content -->
  293. <spry-component name="CardComponent" sid="my-card">
  294. <p>This paragraph is injected into the card's spry-content area.</p>
  295. <button spry-action=":DoSomething">Click me</button>
  296. </spry-component>""";
  297. // Context example
  298. var context_example = get_component_child<CodeBlockComponent>("context-example");
  299. context_example.language = "HTML";
  300. context_example.code = """<!-- Mark properties to be preserved across actions -->
  301. <spry-context property="demo_component_name"/>
  302. <spry-context property="source_file"/>
  303. <div sid="host">
  304. <span content-expr="this.demo_component_name">Demo</span>
  305. <button spry-action=":ShowSource" spry-target="host">Show Source</button>
  306. </div>""";
  307. // Static resources example
  308. var res_example = get_component_child<CodeBlockComponent>("res-example");
  309. res_example.language = "HTML";
  310. res_example.code = """<script spry-res="htmx.js"></script>
  311. <script spry-res="htmx-sse.js"></script>""";
  312. }
  313. }