using Astralis; using Invercargill; using Invercargill.DataStructures; using Inversion; using Spry; /** * TemplateExample.vala - Demonstrates PageComponent and PageTemplate usage * * This example shows how to create template-based pages where: * - MainLayoutTemplate provides the base HTML structure with header and footer * - AdminSectionTemplate adds an admin navigation sidebar (nested template) * - PageComponents render their content into template outlets * * Route structure: * / -> HomePage (uses MainLayoutTemplate) * /about -> AboutPage (uses MainLayoutTemplate) * /admin -> AdminDashboardPage (uses AdminSectionTemplate -> MainLayoutTemplate) * /admin/users -> AdminUsersPage (uses AdminSectionTemplate -> MainLayoutTemplate) */ // ============================================================================= // STYLESHEETS - CSS content served as FastResources // ============================================================================= /** * Main CSS - Base styles for all pages */ private const string MAIN_CSS = """ /* Base Reset & Layout */ * { box-sizing: border-box; margin: 0; padding: 0; } html, body { height: 100%; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; background: #f5f7fa; display: flex; flex-direction: column; min-height: 100vh; } /* Header */ header { background: #2c3e50; color: white; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } header nav a { color: white; margin-right: 1.5rem; text-decoration: none; font-weight: 500; transition: opacity 0.2s; } header nav a:hover { opacity: 0.8; } header nav a:last-child { margin-right: 0; } header .visit-info { font-size: 0.85rem; opacity: 0.8; } /* Main Content - grows to fill space */ main.container { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: 2rem 1rem; } /* Cards */ .card { background: white; border-radius: 12px; padding: 2rem; margin-bottom: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.08); } /* Typography */ h1 { color: #2c3e50; margin-bottom: 1rem; } h2 { color: #34495e; margin-bottom: 0.75rem; margin-top: 1.5rem; } h3 { color: #34495e; margin-bottom: 0.5rem; margin-top: 1rem; } p { color: #555; margin-bottom: 1rem; } ul, ol { margin-left: 1.5rem; margin-bottom: 1rem; } li { margin-bottom: 0.5rem; color: #555; } /* Links */ a { color: #3498db; text-decoration: none; transition: color 0.2s; } a:hover { color: #2980b9; text-decoration: underline; } /* Code */ code { background: #f0f0f0; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.9em; color: #e74c3c; } pre { background: #263238; color: #aed581; padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: 0.9rem; margin: 1rem 0; } /* Footer - at bottom */ footer { background: #2c3e50; color: rgba(255,255,255,0.7); padding: 1.5rem; text-align: center; flex-shrink: 0; } /* Buttons & Forms */ button { background: #3498db; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 6px; cursor: pointer; font-size: 1rem; font-weight: 500; transition: all 0.2s; } button:hover { background: #2980b9; } input[type="text"] { padding: 0.75rem; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 1rem; transition: border-color 0.2s; } input[type="text"]:focus { outline: none; border-color: #3498db; } """; /** * Admin CSS - Additional styles for admin section */ private const string ADMIN_CSS = """ /* Admin Section Layout */ .admin-section { display: flex; gap: 2rem; } /* Admin Sidebar */ .admin-sidebar { width: 220px; background: #34495e; padding: 1.5rem; border-radius: 12px; flex-shrink: 0; } .admin-sidebar h3 { color: white; margin-bottom: 1rem; font-size: 1.1rem; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 0.75rem; } .admin-sidebar ul { list-style: none; margin: 0; padding: 0; } .admin-sidebar li { margin-bottom: 0.5rem; margin-left: 0; } .admin-sidebar a { color: rgba(255,255,255,0.7); display: block; padding: 0.5rem 0.75rem; border-radius: 6px; transition: all 0.2s; } .admin-sidebar a:hover { color: white; background: rgba(255,255,255,0.1); text-decoration: none; } /* Admin Content Area */ .admin-content { flex: 1; min-width: 0; } /* Dashboard Stats */ .dashboard-stats { display: flex; gap: 1.5rem; flex-wrap: wrap; } .stat-card { background: #3498db; color: white; padding: 1.5rem; border-radius: 12px; min-width: 180px; } .stat-card h3 { color: rgba(255,255,255,0.9); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; margin: 0 0 0.5rem 0; } .stat-card .stat-value { font-size: 2.5rem; font-weight: bold; } /* User List */ .user-list { margin: 1.5rem 0; } .user-item { display: flex; align-items: center; gap: 1rem; padding: 1rem 1.25rem; background: #f8f9fa; margin-bottom: 0.75rem; border-radius: 8px; transition: background 0.2s; } .user-item:hover { background: #e9ecef; } .user-item .user-name { flex: 1; font-weight: 500; color: #2c3e50; } .user-item .user-status { color: #27ae60; font-size: 0.9rem; font-weight: 500; } /* Add Form */ .add-form { display: flex; gap: 0.75rem; margin-top: 1.5rem; } .add-form input[type="text"] { flex: 1; max-width: 300px; } /* Navigation Links */ .nav-link { display: inline-block; margin-top: 1.5rem; color: #3498db; font-weight: 500; } .nav-link:hover { color: #2980b9; } """; // ============================================================================= // STORES - Simple data stores for demonstration // ============================================================================= class AppState : Object { public int visit_count { get; set; } public string last_visit { get; set; } } class UserStore : Object { private Series users = new Series(); construct { users.add("Alice"); users.add("Bob"); users.add("Charlie"); } public Enumerable get_all() { return users; } public void add_user(string name) { users.add(name); } public int count() { return (int)users.length; } } // ============================================================================= // TEMPLATES - Provide reusable page layouts // ============================================================================= /** * MainLayoutTemplate - The base template for all pages * * Provides: * - HTML document structure * - Common elements (scripts, styles) * - Site-wide header with navigation * - Site-wide footer * * Uses where child content/templates will be inserted */ class MainLayoutTemplate : PageTemplate { private AppState app_state = inject(); public override string markup { get { return """ Spry Template Example

Built with Spry Framework - Template Example

"""; }} public override async void prepare() throws Error { this["visit-info"].text_content = @"Visits: $(app_state.visit_count) | Last: $(app_state.last_visit)"; } } /** * AdminSectionTemplate - A nested template for admin pages * * This template is applied to routes starting with /admin * It adds an admin sidebar navigation around the page content * * Templates are applied in order of rank (based on prefix length): * 1. MainLayoutTemplate (rank 0, prefix "") - outermost * 2. AdminSectionTemplate (rank 1, prefix "/admin") - nested inside main * 3. PageComponent - innermost content */ class AdminSectionTemplate : PageTemplate { public override string markup { get { return """
"""; }} } // ============================================================================= // PAGE COMPONENTS - Render content into templates // ============================================================================= /** * HomePage - The main landing page * * Extends PageComponent (not Component) to automatically use template rendering * The handle_request method in PageComponent: * 1. Finds all matching templates (MainLayoutTemplate matches "") * 2. Renders each template in order * 3. Inserts each rendered content into the previous template's outlet */ class HomePage : PageComponent { private AppState app_state = inject(); public const string ROUTE = "/"; public override string markup { get { return """

Welcome to Spry!

This example demonstrates template-based pages using PageComponent and PageTemplate.

Features

  • MainLayoutTemplate - Provides base HTML, header, footer
  • AdminSectionTemplate - Adds sidebar for /admin/* routes
  • PageComponent - Pages automatically inherit templates

Try These Pages

"""; }} public override async void prepare() throws Error { app_state.visit_count++; app_state.last_visit = new DateTime.now_local().format("%H:%M:%S"); this["visit-count"].text_content = @"You have visited $(app_state.visit_count) times."; } } /** * AboutPage - A simple about page */ class AboutPage : PageComponent { public const string ROUTE = "/about"; public override string markup { get { return """

About Spry Templates

How Templates Work

Templates use to define where child content will be inserted.

Template Resolution

When a PageComponent handles a request:

  1. It finds all registered PageTemplates matching the route
  2. Templates are ordered by their prefix length (rank)
  3. Each template is rendered, with content nested into outlets

Route Matching

Prefix "" matches all routes
Prefix "/admin" matches /admin and /admin/*
Prefix "/admin/users" matches /admin/users only
            
Back to Home
"""; }} } /** * AdminDashboardPage - Admin dashboard with statistics */ class AdminDashboardPage : PageComponent { private UserStore user_store = inject(); public const string ROUTE = "/admin"; public override string markup { get { return """

Admin Dashboard

Total Users

Back to Home
"""; }} public override async void prepare() throws Error { this["user-count"].text_content = user_store.count().to_string(); } } /** * AdminUsersPage - Admin page with interactive user list * * Demonstrates HTMX interactions within a templated page. * The form targets the card with outerHTML swap so the entire card * is replaced with the updated content. */ class AdminUsersPage : PageComponent { private UserStore user_store = inject(); private ComponentFactory factory = inject(); private HttpContext http_context = inject(); public const string ROUTE = "/admin/users"; public override string markup { get { return """

User Management

Back to Dashboard
"""; }} public override async void prepare() throws Error { refresh_user_list(); } private void refresh_user_list() throws Error { var users = user_store.get_all(); var items = new Series(); users.iterate((name) => { var item = factory.create(); item.username = name; items.add(item); }); set_outlet_children("user-list", items); } public async override void handle_action(string action) throws Error { if (action == "AddUser") { var username = http_context.request.query_params.get_any_or_default("username"); if (username != null && username.length > 0) { user_store.add_user(username); } refresh_user_list(); } } } /** * UserItemComponent - Individual user item in the list */ class UserItemComponent : Component { public string username { set; get; } public override string markup { get { return """
Active
"""; }} public override async void prepare() throws Error { this["name"].text_content = username; } } // ============================================================================= // APPLICATION SETUP // ============================================================================= void main(string[] args) { int port = args.length > 1 ? int.parse(args[1]) : 8080; print("═══════════════════════════════════════════════════════════════\n"); print(" Spry Template Example (PageComponent + PageTemplate)\n"); print("═══════════════════════════════════════════════════════════════\n"); print(" Port: %d\n", port); print("═══════════════════════════════════════════════════════════════\n"); print(" Endpoints:\n"); print(" / - Home page (MainLayoutTemplate)\n"); print(" /about - About page (MainLayoutTemplate)\n"); print(" /admin - Admin dashboard (AdminSectionTemplate)\n"); print(" /admin/users - User management (AdminSectionTemplate)\n"); print(" /styles/main.css - Main stylesheet (FastResource)\n"); print(" /styles/admin.css - Admin stylesheet (FastResource)\n"); print("═══════════════════════════════════════════════════════════════\n"); print("\nPress Ctrl+C to stop the server\n\n"); try { var application = new WebApplication(port); // Enable compression application.use_compression(); // Add Spry module for component actions application.add_module(); // Register stores as singletons application.add_singleton(); application.add_singleton(); // Register templates with route prefixes // MainLayoutTemplate applies to ALL routes (empty prefix) var spry_cfg = application.configure_with(); spry_cfg.add_template(""); // AdminSectionTemplate applies to /admin/* routes spry_cfg.add_template("/admin"); // Register page components as endpoints application.add_transient(); application.add_endpoint(new EndpointRoute(HomePage.ROUTE)); application.add_transient(); application.add_endpoint(new EndpointRoute(AboutPage.ROUTE)); application.add_transient(); application.add_endpoint(new EndpointRoute(AdminDashboardPage.ROUTE)); application.add_transient(); application.add_endpoint(new EndpointRoute(AdminUsersPage.ROUTE)); // Register child components application.add_transient(); // Register CSS as FastResources application.add_startup_endpoint(new EndpointRoute("/styles/main.css"), () => { try { return new FastResource.from_string(MAIN_CSS) .with_content_type("text/css; charset=utf-8") .with_default_compressors(); } catch (Error e) { error("Failed to create main CSS resource: %s", e.message); } }); application.add_startup_endpoint(new EndpointRoute("/styles/admin.css"), () => { try { return new FastResource.from_string(ADMIN_CSS) .with_content_type("text/css; charset=utf-8") .with_default_compressors(); } catch (Error e) { error("Failed to create admin CSS resource: %s", e.message); } }); application.run(); } catch (Error e) { printerr("Error: %s\n", e.message); Process.exit(1); } }