# Important Notes on Writing Documentation Pages for Spry This document captures key learnings and patterns for creating documentation pages in the Spry demo site. ## Page Structure ### PageComponent Pattern Documentation pages extend `PageComponent` (not `Component`). PageComponent is special because it acts as both a Component AND an Endpoint, meaning it handles its own route. ```vala public class Demo.Pages.ComponentsOverviewPage : PageComponent { public override string markup { get { return """..."""; }} public override async void prepare() throws Error { // Setup code } } ``` ### Template Wrapping **IMPORTANT**: Page markup should NOT include the full HTML structure (``, ``, ``, ``). The `MainTemplate` class automatically wraps page content with the proper HTML structure, including: - DOCTYPE and html tags - `` with stylesheets and scripts - `` with the page container Your page markup should start with the content container: ```vala public override string markup { get { return """
"""; }} ``` ### Navigation Sidebar Every documentation page should include the NavSidebarComponent and set its `current_path` property in `prepare()`: ```vala public override async void prepare() throws Error { var nav = get_component_child("nav"); nav.current_path = "/components/overview"; } ``` ## Required Imports ```vala using Spry; using Astralis; // Required for HttpContext using Inversion; // Required for inject() using Invercargill.DataStructures; // Required for Series ``` ## String Handling and Code Examples ### Triple-Quote String Escaping **CRITICAL**: Vala's triple-quoted strings (`"""`) CANNOT contain another triple-quoted string sequence. This means you cannot embed code examples that show triple-quoted strings directly. **Wrong** (will fail to compile): ```vala // This breaks because """ appears inside """ return """

public override string markup { get {
    return """
content
"""; // SYNTAX ERROR! }}
"""; ``` **Solution**: Use regular strings with `\n` for line breaks and string concatenation: ```vala private const string COMPONENT_CODE = "public class MyComponent : Component {\n" + " public override string markup { get {\n" + " return \"\"\"
content
\"\"\";\n" + " }}\n" + "}"; ``` ### Escaping Quotes in Code Examples When code contains double quotes, you need to escape them: - In triple-quoted strings: Use `\"` for literal quotes - In regular strings: Use `\\\"` (escaped backslash + escaped quote) ```vala // Inside a triple-quoted string: "this[\"element\"].text_content = \"Hello\";" // Inside a regular string (more escaping needed): "this[\\\"element\\\"].text_content = \\\"Hello\\\";" ``` ### JSON in Code Examples JSON strings in code examples need heavy escaping: ```vala // Showing hx-vals in code: "this[\"button\"].set_attribute(\"hx-vals\", \"{\\\"id\\\": 123}\");" ``` ## CodeBlockComponent Usage Use `CodeBlockComponent` for displaying code examples with syntax highlighting: ```vala // In markup: "" // In prepare(): var code_block = get_component_child("example-code"); code_block.language = "vala"; code_block.code = YOUR_CODE_STRING; ``` Supported languages: `vala`, `xml`, `css`, `javascript` ## DemoHostComponent Usage Use `DemoHostComponent` for interactive demos that show source code alongside a working demo: ```vala // In markup: "" // In prepare(): var demo = get_component_child("demo"); demo.source_file = "DemoComponents/SimpleCounterDemo.vala"; // Create and set the actual demo component var counter = factory.create(); demo.set_outlet_child("demo-outlet", counter); ``` The DemoHostComponent provides: - Tabbed interface (Source / Demo) - Syntax-highlighted source code display - Live demo outlet ## Component Lifecycle and State ### New Instance Per Request **IMPORTANT**: Components are instantiated fresh on each request. You cannot store state directly in component instance fields between requests. **Wrong** (state won't persist): ```vala public class MyComponent : Component { private int counter = 0; // Reset to 0 on every request! } ``` **Correct** (use a singleton store): ```vala public class MyComponent : Component { private MyStore store = inject(); // Singleton, persists } public class MyStore : Object { public int counter { get; set; default = 0; } // Persists between requests } ``` ### prepare() Must Be Async The `prepare()` method must be marked `async`: ```vala public override async void prepare() throws Error { // ... } ``` ## Dependency Injection Use the `inject()` pattern for dependency injection: ```vala private ComponentFactory factory = inject(); private HttpContext http_context = inject(); private MyStore store = inject(); // Must be registered in Main.vala ``` Register singleton stores in `demo/Main.vala`: ```vala // In Main.vala configure() method: container.register(new MyStore()); ``` ## Series Collection The `Series` type from Invercargill is used for collections: ```vala public Series items { get; set; default = new Series(); } // Add items: items.add(new TodoItem(1, "Task")); // Iterate: foreach (var item in items) { // ... } // Get count: int count = items.length; // NOT .size! // Create new series: var new_items = new Series(); ``` ## Common Patterns ### Element Selection and Modification ```vala // Select by sid: this["my-element"].text_content = "Hello"; this["my-element"].add_class("active"); this["my-element"].set_attribute("data-id", "123"); // Set inner HTML: this["container"].inner_html = "Dynamic content"; ``` ### Handling Actions ```vala public async override void handle_action(string action) throws Error { switch (action) { case "Save": // Get form data var value = http_context.request.query_params.get_any_or_default("field_name"); // Process... break; case "Delete": // ... break; } } ``` ### Using Outlets for Child Components ```vala // In markup: "
" // In prepare(): var children = new Series(); foreach (var item in store.items) { var component = factory.create(); component.item_id = item.id; children.add(component); } set_outlet_children("items-outlet", children); ``` ## Routes and Registration ### Route Convention Routes follow the pattern `/section/page-name`: - `/components/overview` - `/components/actions` - `/components/outlets` ### Registration in Main.vala Pages are registered with the router: ```vala // In Main.vala: router.add_route("/components/overview", typeof(ComponentsOverviewPage)); ``` Components used in demos must be registered with the factory: ```vala factory.register(); factory.register(); ``` ## File Organization ``` demo/ ├── Pages/ # PageComponent classes │ ├── ComponentsOverviewPage.vala │ ├── ComponentsActionsPage.vala │ └── ... ├── Components/ # Shared UI components │ ├── NavSidebarComponent.vala │ ├── CodeBlockComponent.vala │ └── DemoHostComponent.vala ├── DemoComponents/ # Demo-specific components │ ├── SimpleCounterDemo.vala │ ├── TodoListDemo.vala │ └── ProgressDemo.vala ├── Static/ │ └── docs.css # Documentation styles ├── Main.vala # App configuration and routing ├── MainTemplate.vala # HTML wrapper template └── meson.build # Build configuration ``` ## Build Configuration Add new source files to `demo/meson.build`: ```vala demo_sources = files( 'Main.vala', 'MainTemplate.vala', 'Pages/ComponentsOverviewPage.vala', 'Pages/ComponentsActionsPage.vala', 'DemoComponents/SimpleCounterDemo.vala', # ... etc ) ``` ## Styling Conventions Documentation pages use CSS classes from `docs.css`: - `.doc-hero` - Hero section with title - `.doc-section` - Content sections - `.doc-card` - Card containers - `.doc-table` - Tables for reference - `.doc-example` - Code example containers - `.demo-container` - Interactive demo containers ## Debugging Tips 1. **Build errors with strings**: Check for unescaped quotes or triple-quote conflicts 2. **inject() not found**: Add `using Inversion;` 3. **HttpContext not found**: Add `using Astralis;` 4. **Property read-only**: Add `set;` to property definition 5. **Method signature mismatch**: Ensure `prepare()` is `async` ## Quick Reference: Page Template ```vala using Spry; using Astralis; using Inversion; public class Demo.Pages.YourPage : PageComponent { private ComponentFactory factory = inject(); public override string markup { get { return """

Your Page Title

Brief description

Section Title

Content...

"""; }} public override async void prepare() throws Error { var nav = get_component_child("nav"); nav.current_path = "/your/path"; var code = get_component_child("example"); code.language = "vala"; code.code = "your code here"; } } ```