important-notes-on-writing-documentation-pages.md 10 KB

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.

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 (<!DOCTYPE html>, <html>, <head>, <body>). The MainTemplate class automatically wraps page content with the proper HTML structure, including:

  • DOCTYPE and html tags
  • <head> with stylesheets and scripts
  • <body> with the page container

Your page markup should start with the content container:

public override string markup { get {
    return """
    <div class="page-container">
        <spry-component name="NavSidebarComponent" sid="nav"/>
        <main class="main-content">
            <!-- Your content here -->
        </main>
    </div>
    """;
}}

Navigation Sidebar

Every documentation page should include the NavSidebarComponent and set its current_path property in prepare():

public override async void prepare() throws Error {
    var nav = get_component_child<NavSidebarComponent>("nav");
    nav.current_path = "/components/overview";
}

Required Imports

using Spry;
using Astralis;      // Required for HttpContext
using Inversion;     // Required for inject<T>()
using Invercargill.DataStructures;  // Required for Series<T>

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):

// This breaks because """ appears inside """
return """
    <pre><code>
public override string markup { get {
    return """<div>content</div>""";  // SYNTAX ERROR!
}}
    </code></pre>
""";

Solution: Use regular strings with \n for line breaks and string concatenation:

private const string COMPONENT_CODE = 
    "public class MyComponent : Component {\n" +
    "    public override string markup { get {\n" +
    "        return \"\"\"<div>content</div>\"\"\";\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)

    // 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:

// Showing hx-vals in code:
"this[\"button\"].set_attribute(\"hx-vals\", \"{\\\"id\\\": 123}\");"

CodeBlockComponent Usage

Use CodeBlockComponent for displaying code examples with syntax highlighting:

// In markup:
"<spry-component name=\"CodeBlockComponent\" sid=\"example-code\"/>"

// In prepare():
var code_block = get_component_child<CodeBlockComponent>("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:

// In markup:
"<spry-component name=\"DemoHostComponent\" sid=\"demo\"/>"

// In prepare():
var demo = get_component_child<DemoHostComponent>("demo");
demo.source_file = "DemoComponents/SimpleCounterDemo.vala";

// Create and set the actual demo component
var counter = factory.create<SimpleCounterDemo>();
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):

public class MyComponent : Component {
    private int counter = 0;  // Reset to 0 on every request!
}

Correct (use a singleton store):

public class MyComponent : Component {
    private MyStore store = inject<MyStore>();  // 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:

public override async void prepare() throws Error {
    // ...
}

Dependency Injection

Use the inject<T>() pattern for dependency injection:

private ComponentFactory factory = inject<ComponentFactory>();
private HttpContext http_context = inject<HttpContext>();
private MyStore store = inject<MyStore>();  // Must be registered in Main.vala

Register singleton stores in demo/Main.vala:

// In Main.vala configure() method:
container.register<MyStore>(new MyStore());

Series Collection

The Series<T> type from Invercargill is used for collections:

public Series<TodoItem> items { get; set; default = new Series<TodoItem>(); }

// 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<TodoItem>();

Common Patterns

Element Selection and Modification

// 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 = "<span>Dynamic content</span>";

Handling Actions

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

// In markup:
"<ul><spry-outlet sid=\"items-outlet\"/></ul>"

// In prepare():
var children = new Series<Renderable>();
foreach (var item in store.items) {
    var component = factory.create<ChildComponent>();
    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:

// In Main.vala:
router.add_route("/components/overview", typeof(ComponentsOverviewPage));

Components used in demos must be registered with the factory:

factory.register<SimpleCounterDemo>();
factory.register<TodoListDemo>();

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:

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
  6. Quick Reference: Page Template

    using Spry;
    using Astralis;
    using Inversion;
    
    public class Demo.Pages.YourPage : PageComponent {
        
        private ComponentFactory factory = inject<ComponentFactory>();
        
        public override string markup { get {
            return """
            <div class="page-container">
                <spry-component name="NavSidebarComponent" sid="nav"/>
                <main class="main-content">
                    <div class="doc-hero">
                        <h1>Your Page Title</h1>
                        <p class="doc-subtitle">Brief description</p>
                    </div>
                    
                    <div class="doc-section">
                        <h2>Section Title</h2>
                        <p>Content...</p>
                        
                        <spry-component name="CodeBlockComponent" sid="example"/>
                    </div>
                </main>
            </div>
            """;
        }}
        
        public override async void prepare() throws Error {
            var nav = get_component_child<NavSidebarComponent>("nav");
            nav.current_path = "/your/path";
            
            var code = get_component_child<CodeBlockComponent>("example");
            code.language = "vala";
            code.code = "your code here";
        }
    }