# 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
""";
}}
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";
}
}
```