Chapter 5: Page Navigation#
A Single-Page App Inside Your Desktop App#
Your application has a dashboard, a user management page, a settings page, and a reports page. In a traditional Delphi app, you create four forms and show/hide them. With TTina4HTMLPages, you create four pages inside one HTML renderer and navigate between them with anchor links -- the same way a single-page web app works.
No form switching. No component visibility toggling. No frame loading. Just set the HTML for each page, link them to a renderer, and navigate with <a href="#pagename"> or programmatic calls.
1. TTina4HTMLPages Basics#
TTina4HTMLPages manages a collection of pages and renders them through a TTina4HTMLRender. Each page has a name, HTML content (or Twig template content), and an optional "is default" flag.
Setup#
Drop two components on your form:
- TTina4HTMLRender (name:
HTMLRender1) -- setAligntoClient - TTina4HTMLPages (name:
HTMLPages1)
Link them:
HTMLPages1.Renderer := HTMLRender1;That is the entire setup. The pages component controls what the renderer displays.
2. Creating Pages at Design Time#
Double-click HTMLPages1 in the form designer to open the collection editor. Click "Add" to create pages:
Page 1:
PageName:homeIsDefault:TrueHTMLContent:<h1>Home</h1><p>Welcome to the app.</p><a href="#settings">Go to Settings</a>
Page 2:
PageName:settingsIsDefault:FalseHTMLContent:<h1>Settings</h1><p>Configure your preferences.</p><a href="#home">Back to Home</a>
Run the app. The home page appears (it has IsDefault = True). Click "Go to Settings". The settings page appears. Click "Back to Home". You are back. No code written.
3. Creating Pages at Runtime#
For dynamic apps where pages are built from data or configuration:
procedure TForm1.FormCreate(Sender: TObject);var Page: TTina4Page;begin HTMLPages1.Renderer := HTMLRender1;โ // Home page Page := HTMLPages1.Pages.Add; Page.PageName := 'home'; Page.IsDefault := True; Page.HTMLContent.Text := '<style>' + ' .nav { background: #1a1a2e; padding: 12px 20px; }' + ' .nav a { color: white; margin-right: 16px; text-decoration: none; }' + ' .content { padding: 20px; }' + '</style>' + '<div class="nav">' + ' <a href="#home">Home</a>' + ' <a href="#users">Users</a>' + ' <a href="#settings">Settings</a>' + '</div>' + '<div class="content">' + ' <h1>Dashboard</h1>' + ' <p>Welcome back. Select a section from the navigation above.</p>' + '</div>';โ // Users page Page := HTMLPages1.Pages.Add; Page.PageName := 'users'; Page.HTMLContent.Text := '<div class="nav">' + ' <a href="#home">Home</a>' + ' <a href="#users">Users</a>' + ' <a href="#settings">Settings</a>' + '</div>' + '<div class="content">' + ' <h1>Users</h1>' + ' <p>User management goes here.</p>' + '</div>';โ // Settings page Page := HTMLPages1.Pages.Add; Page.PageName := 'settings'; Page.HTMLContent.Text := '<div class="nav">' + ' <a href="#home">Home</a>' + ' <a href="#users">Users</a>' + ' <a href="#settings">Settings</a>' + '</div>' + '<div class="content">' + ' <h1>Settings</h1>' + ' <p>Application settings go here.</p>' + '</div>';end;4. Setting the Default Page#
The page with IsDefault := True renders automatically when the component initializes. If no page has IsDefault set, nothing renders until you navigate programmatically.
Page.IsDefault := True;Only one page should have IsDefault = True. If multiple pages have it set, the first one in the collection wins.
5. Navigation via Anchor Links#
The most natural way to navigate is with HTML anchor tags. The href value maps to a page name:
href value | Maps to PageName |
|---|---|
#dashboard | dashboard |
/settings | settings |
about | about |
The renderer intercepts link clicks and delegates to the pages component. The leading # or / is stripped to match the PageName.
<a href="#home">Home</a><a href="#users">Users</a><a href="/settings">Settings</a>All three formats work. The # prefix is conventional for SPA-style navigation. The / prefix works the same way. A bare name also works.
6. Programmatic Navigation#
Navigate from Delphi code without user interaction:
HTMLPages1.NavigateTo('settings');This is useful for:
- Redirecting after a successful action (e.g., login redirects to dashboard)
- Responding to button clicks that are not anchor tags
- Implementing back/forward functionality
- Conditional navigation based on business logic
procedure TForm1.OnLoginSuccess;begin HTMLPages1.NavigateTo('dashboard');end;โprocedure TForm1.OnLogout;begin HTMLPages1.NavigateTo('login');end;Reading the Active Page#
var CurrentPage := HTMLPages1.ActivePage;if CurrentPage = 'settings' then ShowMessage('You are on the settings page');7. Page Properties#
Each TTina4Page in the collection has these properties:
| Property | Type | Description |
|---|---|---|
PageName | string | Unique name used as navigation target |
TwigContent | TStringList | Twig template source (rendered via TTina4Twig) |
HTMLContent | TStringList | Raw HTML (used when TwigContent is empty) |
IsDefault | Boolean | If True, this page is shown on startup |
Priority rule: If TwigContent is not empty, it is rendered through the Twig engine and the result replaces HTMLContent. If TwigContent is empty, HTMLContent is used directly.
8. Component Properties#
TTina4HTMLPages itself has these properties:
| Property | Type | Description |
|---|---|---|
Pages | TTina4PageCollection | Collection of pages (design-time editable) |
Renderer | TTina4HTMLRender | The HTML renderer that displays the active page |
ActivePage | string | Name of the currently displayed page (read/write) |
| TwigTemplatePath | string | Base path for Twig {% include %} / {% extends %} |
9. Events#
OnBeforeNavigate#
Fires before navigation occurs. Set Allow := False to cancel the navigation:
procedure TForm1.HTMLPages1BeforeNavigate(Sender: TObject; const FromPage, ToPage: string; var Allow: Boolean);begin // Prevent navigation to admin page if not authenticated if (ToPage = 'admin') and (not FIsAuthenticated) then begin Allow := False; ShowMessage('You must log in to access the admin page.'); HTMLPages1.NavigateTo('login'); end;end;OnAfterNavigate#
Fires after the new page has been rendered. Use it for post-navigation setup like loading data:
procedure TForm1.HTMLPages1AfterNavigate(Sender: TObject);begin if HTMLPages1.ActivePage = 'users' then LoadUserData; if HTMLPages1.ActivePage = 'dashboard' then RefreshStats;end;10. Using Twig Templates in Pages#
Pages can use Twig templates for dynamic content. Set variables with SetTwigVariable and use Twig syntax in TwigContent:
HTMLPages1.SetTwigVariable('userName', 'Alice');HTMLPages1.SetTwigVariable('userRole', 'Admin');HTMLPages1.SetTwigVariable('notificationCount', '3');โvar Page := HTMLPages1.Pages.Add;Page.PageName := 'dashboard';Page.TwigContent.Text := '<div class="header">' + ' <h1>Welcome, {{ userName }}</h1>' + ' <span>Role: {{ userRole }}</span>' + ' {% if notificationCount > 0 %}' + ' <span class="badge">{{ notificationCount }} new</span>' + ' {% endif %}' + '</div>';File-Based Templates#
For complex pages, use files with {% include %} and {% extends %}:
HTMLPages1.TwigTemplatePath := 'C:\MyApp\templates';โvar Page := HTMLPages1.Pages.Add;Page.PageName := 'report';Page.TwigContent.LoadFromFile('C:\MyApp\templates\report.html');report.html:
{% extends 'layout.html' %}โ{% block title %}Monthly Report{% endblock %}โ{% block content %} <h1>Report for {{ month }}</h1> <table> {% for row in data %} <tr> <td>{{ row.name }}</td> <td>{{ row.value }}</td> </tr> {% endfor %} </table>{% endblock %}layout.html:
<html><head> <style> body { font-family: Arial, sans-serif; } .nav { background: #333; padding: 12px; } .nav a { color: white; margin-right: 16px; } </style></head><body> <div class="nav"> <a href="#home">Home</a> <a href="#report">Report</a> <a href="#settings">Settings</a> </div> <div style="padding: 20px;"> <title>{% block title %}{% endblock %}</title> {% block content %}{% endblock %} </div></body></html>11. Complete Example: Multi-Page Admin App#
A full admin application with a sidebar menu, dashboard page, users page with a data table, and settings page with a form.
unit AdminApp;โinterfaceโuses System.SysUtils, System.Classes, System.JSON, FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, Tina4HTMLRender, Tina4HTMLPages, Tina4REST;โtype TfrmAdmin = class(TForm) HTMLRender1: TTina4HTMLRender; HTMLPages1: TTina4HTMLPages; restAPI: TTina4REST; procedure FormCreate(Sender: TObject); procedure HTMLPages1AfterNavigate(Sender: TObject); procedure HTMLRender1FormSubmit(Sender: TObject; const FormName: string; FormData: TStrings); private function BuildLayout(const ActivePage, Content: string): string; function GetSidebar(const ActivePage: string): string; function GetStyles: string; procedure SetupPages; published procedure NavTo(PageName: String); procedure EditUser(UserId: String); procedure SaveSettings(Action: String); end;โvar frmAdmin: TfrmAdmin;โimplementationโ{$R *.fmx}โprocedure TfrmAdmin.FormCreate(Sender: TObject);begin restAPI.BaseUrl := 'https://api.example.com/v1'; HTMLPages1.Renderer := HTMLRender1; HTMLRender1.RegisterObject('Admin', Self); SetupPages;end;โfunction TfrmAdmin.GetStyles: string;begin Result := '<style>' + ' * { box-sizing: border-box; margin: 0; padding: 0; }' + ' body { font-family: Arial, sans-serif; display: inline-block; width: 100%; }' + ' .sidebar { display: inline-block; width: 200px; background: #1a1a2e;' + ' min-height: 600px; padding: 20px 0; vertical-align: top; }' + ' .sidebar h3 { color: #7c8db5; padding: 0 20px 16px; font-size: 12px;' + ' text-transform: uppercase; letter-spacing: 1px; }' + ' .sidebar a { display: block; color: #a0aec0; padding: 10px 20px;' + ' text-decoration: none; font-size: 14px; }' + ' .sidebar a:hover, .sidebar a.active { background: #16213e; color: white; }' + ' .main { display: inline-block; width: calc(100% - 220px); padding: 24px;' + ' vertical-align: top; }' + ' .page-title { font-size: 24px; color: #1a1a2e; margin-bottom: 20px; }' + ' .card { background: white; border: 1px solid #e2e8f0; border-radius: 8px;' + ' padding: 20px; margin-bottom: 16px; }' + ' .stat-row { display: inline-block; width: 100%; margin-bottom: 20px; }' + ' .stat { display: inline-block; width: 30%; margin-right: 3%;' + ' background: #f8fafc; border-radius: 8px; padding: 16px; text-align: center; }' + ' .stat .number { font-size: 32px; font-weight: bold; color: #2563eb; }' + ' .stat .label { font-size: 12px; color: #888; margin-top: 4px; }' + ' table { width: 100%; border-collapse: collapse; }' + ' th { text-align: left; padding: 10px; border-bottom: 2px solid #e2e8f0;' + ' font-size: 12px; color: #888; text-transform: uppercase; }' + ' td { padding: 10px; border-bottom: 1px solid #f0f0f0; }' + ' .btn { padding: 6px 14px; border: none; border-radius: 4px; font-size: 13px; }' + ' .btn-primary { background: #2563eb; color: white; }' + ' .btn-sm { padding: 4px 10px; font-size: 12px; }' + ' .form-group { margin-bottom: 16px; }' + ' .form-group label { display: block; margin-bottom: 4px; font-weight: bold;' + ' font-size: 14px; color: #555; }' + ' .form-group input, .form-group select { width: 100%; padding: 8px;' + ' border: 1px solid #ddd; border-radius: 4px; }' + '</style>';end;โfunction TfrmAdmin.GetSidebar(const ActivePage: string): string;โ function ActiveClass(const Page: string): string; begin if Page = ActivePage then Result := ' class="active"' else Result := ''; end;โbegin Result := '<div class="sidebar">' + ' <h3>Navigation</h3>' + ' <a href="#dashboard"' + ActiveClass('dashboard') + '>Dashboard</a>' + ' <a href="#users"' + ActiveClass('users') + '>Users</a>' + ' <a href="#settings"' + ActiveClass('settings') + '>Settings</a>' + '</div>';end;โfunction TfrmAdmin.BuildLayout(const ActivePage, Content: string): string;begin Result := GetStyles + '<div>' + GetSidebar(ActivePage) + '<div class="main">' + Content + '</div>' + '</div>';end;โprocedure TfrmAdmin.SetupPages;var Page: TTina4Page;begin // Dashboard Page := HTMLPages1.Pages.Add; Page.PageName := 'dashboard'; Page.IsDefault := True; Page.HTMLContent.Text := BuildLayout('dashboard', '<h1 class="page-title">Dashboard</h1>' + '<div class="stat-row">' + ' <div class="stat">' + ' <div class="number" id="userCount">248</div>' + ' <div class="label">Total Users</div>' + ' </div>' + ' <div class="stat">' + ' <div class="number" id="activeCount">189</div>' + ' <div class="label">Active</div>' + ' </div>' + ' <div class="stat">' + ' <div class="number" id="newCount">12</div>' + ' <div class="label">New Today</div>' + ' </div>' + '</div>' + '<div class="card">' + ' <h3>Recent Activity</h3>' + ' <table>' + ' <tr><td>Alice logged in</td><td style="color:#888;">2 min ago</td></tr>' + ' <tr><td>Bob updated profile</td><td style="color:#888;">15 min ago</td></tr>' + ' <tr><td>Carol created a report</td><td style="color:#888;">1 hour ago</td></tr>' + ' </table>' + '</div>');โ // Users Page := HTMLPages1.Pages.Add; Page.PageName := 'users'; Page.HTMLContent.Text := BuildLayout('users', '<h1 class="page-title">User Management</h1>' + '<div class="card">' + ' <table>' + ' <thead>' + ' <tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th>Actions</th></tr>' + ' </thead>' + ' <tbody>' + ' <tr>' + ' <td>Alice Smith</td><td>alice@example.com</td><td>Admin</td>' + ' <td style="color:green;">Active</td>' + ' <td><button class="btn btn-sm btn-primary" ' + ' onclick="Admin:EditUser(''1'')">Edit</button></td>' + ' </tr>' + ' <tr>' + ' <td>Bob Johnson</td><td>bob@example.com</td><td>Editor</td>' + ' <td style="color:green;">Active</td>' + ' <td><button class="btn btn-sm btn-primary" ' + ' onclick="Admin:EditUser(''2'')">Edit</button></td>' + ' </tr>' + ' <tr>' + ' <td>Carol Williams</td><td>carol@example.com</td><td>Viewer</td>' + ' <td style="color:red;">Inactive</td>' + ' <td><button class="btn btn-sm btn-primary" ' + ' onclick="Admin:EditUser(''3'')">Edit</button></td>' + ' </tr>' + ' </tbody>' + ' </table>' + '</div>');โ // Settings Page := HTMLPages1.Pages.Add; Page.PageName := 'settings'; Page.HTMLContent.Text := BuildLayout('settings', '<h1 class="page-title">Settings</h1>' + '<div class="card">' + ' <form name="settingsForm">' + ' <div class="form-group">' + ' <label>Application Name</label>' + ' <input type="text" name="appName" id="appName" value="My Admin App">' + ' </div>' + ' <div class="form-group">' + ' <label>Default Language</label>' + ' <select name="language" id="language">' + ' <option value="en">English</option>' + ' <option value="fr">French</option>' + ' <option value="de">German</option>' + ' </select>' + ' </div>' + ' <div class="form-group">' + ' <label>Items Per Page</label>' + ' <input type="text" name="pageSize" id="pageSize" value="25">' + ' </div>' + ' <button type="submit" class="btn btn-primary">Save Settings</button>' + ' </form>' + '</div>');end;โprocedure TfrmAdmin.HTMLPages1AfterNavigate(Sender: TObject);begin // Load data when navigating to specific pages if HTMLPages1.ActivePage = 'dashboard' then begin // Could refresh stats from API here end;end;โprocedure TfrmAdmin.HTMLRender1FormSubmit(Sender: TObject; const FormName: string; FormData: TStrings);begin if FormName = 'settingsForm' then begin ShowMessage(Format('Settings saved: App=%s, Lang=%s, PageSize=%s', [FormData.Values['appName'], FormData.Values['language'], FormData.Values['pageSize']])); end;end;โprocedure TfrmAdmin.NavTo(PageName: String);begin HTMLPages1.NavigateTo(PageName);end;โprocedure TfrmAdmin.EditUser(UserId: String);begin ShowMessage('Edit user: ' + UserId); // In a real app: navigate to an edit page with the user's data pre-filledend;โprocedure TfrmAdmin.SaveSettings(Action: String);begin ShowMessage('Settings action: ' + Action);end;โend.12. Complete Example: Navigation Guards#
A login-protected app where users must authenticate before accessing protected pages.
unit GuardedApp;โinterfaceโuses System.SysUtils, System.Classes, System.JSON, FMX.Types, FMX.Controls, FMX.Forms, Tina4HTMLRender, Tina4HTMLPages, Tina4REST;โtype TfrmGuarded = class(TForm) HTMLRender1: TTina4HTMLRender; HTMLPages1: TTina4HTMLPages; restAPI: TTina4REST; procedure FormCreate(Sender: TObject); procedure HTMLPages1BeforeNavigate(Sender: TObject; const FromPage, ToPage: string; var Allow: Boolean); procedure HTMLRender1FormSubmit(Sender: TObject; const FormName: string; FormData: TStrings); private FIsLoggedIn: Boolean; FUserName: string; procedure SetupPages; published procedure Logout(Action: String); end;โvar frmGuarded: TfrmGuarded;โimplementationโ{$R *.fmx}โprocedure TfrmGuarded.FormCreate(Sender: TObject);begin FIsLoggedIn := False; FUserName := ''; restAPI.BaseUrl := 'https://api.example.com'; HTMLPages1.Renderer := HTMLRender1; HTMLRender1.RegisterObject('App', Self); SetupPages;end;โprocedure TfrmGuarded.SetupPages;var Page: TTina4Page;begin // Login page (the default -- shown first) Page := HTMLPages1.Pages.Add; Page.PageName := 'login'; Page.IsDefault := True; Page.HTMLContent.Text := '<style>' + ' .login-box { max-width: 360px; margin: 80px auto; padding: 32px;' + ' background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }' + ' .login-box h2 { text-align: center; margin-bottom: 24px; color: #333; }' + ' .field { margin-bottom: 16px; }' + ' .field label { display: block; margin-bottom: 4px; font-size: 14px; color: #555; }' + ' .field input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }' + ' .btn { width: 100%; padding: 12px; background: #2563eb; color: white;' + ' border: none; border-radius: 4px; font-size: 16px; }' + ' .error { color: #dc2626; font-size: 13px; text-align: center;' + ' margin-bottom: 12px; display: none; }' + '</style>' + '<div class="login-box">' + ' <h2>Login Required</h2>' + ' <div class="error" id="loginError">Invalid credentials</div>' + ' <form name="loginForm">' + ' <div class="field">' + ' <label>Username</label>' + ' <input type="text" name="username" id="username" placeholder="Enter username">' + ' </div>' + ' <div class="field">' + ' <label>Password</label>' + ' <input type="password" name="password" id="password" placeholder="Enter password">' + ' </div>' + ' <button type="submit" class="btn">Sign In</button>' + ' </form>' + '</div>';โ // Protected: Dashboard Page := HTMLPages1.Pages.Add; Page.PageName := 'dashboard'; Page.HTMLContent.Text := '<style>' + ' .topbar { background: #1a1a2e; color: white; padding: 12px 20px;' + ' display: inline-block; width: 100%; }' + ' .topbar span { float: left; }' + ' .topbar .user { float: right; }' + ' .topbar a { color: #93c5fd; margin-left: 16px; }' + ' .content { padding: 24px; }' + '</style>' + '<div class="topbar">' + ' <span><b>Admin Panel</b></span>' + ' <span class="user">' + ' <a href="#profile">Profile</a>' + ' <a href="#settings">Settings</a>' + ' <span onclick="App:Logout(''now'')" style="color: #fca5a5; cursor: pointer;' + ' margin-left: 16px;">Logout</span>' + ' </span>' + '</div>' + '<div class="content">' + ' <h1>Dashboard</h1>' + ' <p>You are logged in. This is the protected dashboard.</p>' + ' <p><a href="#profile">View Profile</a> | <a href="#settings">Settings</a></p>' + '</div>';โ // Protected: Profile Page := HTMLPages1.Pages.Add; Page.PageName := 'profile'; Page.HTMLContent.Text := '<div class="topbar">' + ' <span><b>Admin Panel</b></span>' + ' <span class="user">' + ' <a href="#dashboard">Dashboard</a>' + ' <span onclick="App:Logout(''now'')" style="color: #fca5a5; cursor: pointer;' + ' margin-left: 16px;">Logout</span>' + ' </span>' + '</div>' + '<div class="content">' + ' <h1>Profile</h1>' + ' <p>User profile page. Only accessible when logged in.</p>' + ' <p><a href="#dashboard">Back to Dashboard</a></p>' + '</div>';โ // Protected: Settings Page := HTMLPages1.Pages.Add; Page.PageName := 'settings'; Page.HTMLContent.Text := '<div class="topbar">' + ' <span><b>Admin Panel</b></span>' + ' <span class="user">' + ' <a href="#dashboard">Dashboard</a>' + ' <span onclick="App:Logout(''now'')" style="color: #fca5a5; cursor: pointer;' + ' margin-left: 16px;">Logout</span>' + ' </span>' + '</div>' + '<div class="content">' + ' <h1>Settings</h1>' + ' <p>App settings. Only accessible when logged in.</p>' + ' <p><a href="#dashboard">Back to Dashboard</a></p>' + '</div>';end;โprocedure TfrmGuarded.HTMLPages1BeforeNavigate(Sender: TObject; const FromPage, ToPage: string; var Allow: Boolean);begin // The login page is always accessible if ToPage = 'login' then begin Allow := True; Exit; end;โ // All other pages require authentication if not FIsLoggedIn then begin Allow := False; HTMLPages1.NavigateTo('login'); end;end;โprocedure TfrmGuarded.HTMLRender1FormSubmit(Sender: TObject; const FormName: string; FormData: TStrings);var Username, Password: string;begin if FormName <> 'loginForm' then Exit;โ Username := FormData.Values['username']; Password := FormData.Values['password'];โ // Simple validation (in production, call your auth API) if (Username = 'admin') and (Password = 'password') then begin FIsLoggedIn := True; FUserName := Username; HTMLPages1.NavigateTo('dashboard'); end else begin HTMLRender1.SetElementVisible('loginError', True); end;end;โprocedure TfrmGuarded.Logout(Action: String);begin FIsLoggedIn := False; FUserName := ''; HTMLPages1.NavigateTo('login');end;โend.13. Exercise: Wizard / Step-by-Step Form#
Build a wizard with 4 pages (steps), Next/Back navigation, and validation before advancing.
Requirements#
- Step 1: Personal Info (name, email) -- both required
- Step 2: Address (street, city, zip) -- all required
- Step 3: Preferences (language dropdown, newsletter checkbox)
- Step 4: Confirmation (summary of all entered data, submit button)
- "Next" button validates current step before advancing
- "Back" button always works (no validation needed)
- Step indicator showing current position (e.g., "Step 2 of 4")
Solution#
unit WizardForm;โinterfaceโuses System.SysUtils, System.Classes, FMX.Types, FMX.Controls, FMX.Forms, Tina4HTMLRender, Tina4HTMLPages;โtype TfrmWizard = class(TForm) HTMLRender1: TTina4HTMLRender; HTMLPages1: TTina4HTMLPages; procedure FormCreate(Sender: TObject); procedure HTMLPages1BeforeNavigate(Sender: TObject; const FromPage, ToPage: string; var Allow: Boolean); procedure HTMLRender1FormSubmit(Sender: TObject; const FormName: string; FormData: TStrings); private FData: TStringList; procedure SetupPages; function GetStyles: string; function StepHeader(Current: Integer): string; function ValidateStep(const StepName: string): Boolean; published procedure GoNext(FromStep: String); procedure GoBack(FromStep: String); end;โvar frmWizard: TfrmWizard;โimplementationโ{$R *.fmx}โprocedure TfrmWizard.FormCreate(Sender: TObject);begin FData := TStringList.Create; HTMLPages1.Renderer := HTMLRender1; HTMLRender1.RegisterObject('Wizard', Self); SetupPages;end;โfunction TfrmWizard.GetStyles: string;begin Result := '<style>' + ' body { font-family: Arial, sans-serif; background: #f5f5f5; }' + ' .wizard { max-width: 500px; margin: 30px auto; background: white;' + ' border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); overflow: hidden; }' + ' .steps { display: inline-block; width: 100%; background: #f8f9fa;' + ' padding: 16px 20px; border-bottom: 1px solid #e0e0e0; }' + ' .step-dot { display: inline-block; width: 30px; height: 30px; border-radius: 50%;' + ' background: #ddd; color: #888; text-align: center; line-height: 30px;' + ' font-size: 14px; margin-right: 8px; }' + ' .step-dot.active { background: #2563eb; color: white; }' + ' .step-dot.done { background: #16a34a; color: white; }' + ' .step-label { font-size: 13px; color: #888; margin-left: 4px; margin-right: 20px; }' + ' .body { padding: 24px; }' + ' .field { margin-bottom: 16px; }' + ' .field label { display: block; margin-bottom: 4px; font-weight: bold; font-size: 14px; }' + ' .field input, .field select { width: 100%; padding: 8px; border: 1px solid #ccc;' + ' border-radius: 4px; }' + ' .error-text { color: #dc2626; font-size: 12px; display: none; margin-top: 4px; }' + ' .buttons { padding: 16px 24px; border-top: 1px solid #e0e0e0;' + ' display: inline-block; width: 100%; }' + ' .btn { padding: 8px 20px; border: none; border-radius: 4px; font-size: 14px; }' + ' .btn-next { background: #2563eb; color: white; float: right; }' + ' .btn-back { background: #e2e8f0; color: #333; float: left; }' + ' .btn-submit { background: #16a34a; color: white; float: right; }' + ' .summary-row { padding: 8px 0; border-bottom: 1px solid #f0f0f0; }' + ' .summary-label { font-weight: bold; color: #555; display: inline-block; width: 120px; }' + '</style>';end;โfunction TfrmWizard.StepHeader(Current: Integer): string;var Labels: array[1..4] of string; I: Integer; CSSClass: string;begin Labels[1] := 'Personal'; Labels[2] := 'Address'; Labels[3] := 'Preferences'; Labels[4] := 'Confirm';โ Result := '<div class="steps">'; for I := 1 to 4 do begin if I < Current then CSSClass := 'step-dot done' else if I = Current then CSSClass := 'step-dot active' else CSSClass := 'step-dot';โ Result := Result + Format('<span class="%s">%d</span><span class="step-label">%s</span>', [CSSClass, I, Labels[I]]); end; Result := Result + '</div>';end;โprocedure TfrmWizard.SetupPages;var Page: TTina4Page;begin // Step 1: Personal Info Page := HTMLPages1.Pages.Add; Page.PageName := 'step1'; Page.IsDefault := True; Page.HTMLContent.Text := GetStyles + '<div class="wizard">' + StepHeader(1) + '<div class="body">' + ' <h2>Personal Information</h2>' + ' <div class="field">' + ' <label>Full Name</label>' + ' <input type="text" name="name" id="name" placeholder="Your name">' + ' <div class="error-text" id="nameError">Name is required</div>' + ' </div>' + ' <div class="field">' + ' <label>Email</label>' + ' <input type="email" name="email" id="email" placeholder="you@example.com">' + ' <div class="error-text" id="emailError">Valid email is required</div>' + ' </div>' + '</div>' + '<div class="buttons">' + ' <button class="btn btn-next" onclick="Wizard:GoNext(''step1'')">Next</button>' + '</div>' + '</div>';โ // Step 2: Address Page := HTMLPages1.Pages.Add; Page.PageName := 'step2'; Page.HTMLContent.Text := GetStyles + '<div class="wizard">' + StepHeader(2) + '<div class="body">' + ' <h2>Address</h2>' + ' <div class="field">' + ' <label>Street</label>' + ' <input type="text" name="street" id="street">' + ' <div class="error-text" id="streetError">Street is required</div>' + ' </div>' + ' <div class="field">' + ' <label>City</label>' + ' <input type="text" name="city" id="city">' + ' <div class="error-text" id="cityError">City is required</div>' + ' </div>' + ' <div class="field">' + ' <label>Zip Code</label>' + ' <input type="text" name="zip" id="zip">' + ' <div class="error-text" id="zipError">Zip code is required</div>' + ' </div>' + '</div>' + '<div class="buttons">' + ' <button class="btn btn-back" onclick="Wizard:GoBack(''step2'')">Back</button>' + ' <button class="btn btn-next" onclick="Wizard:GoNext(''step2'')">Next</button>' + '</div>' + '</div>';โ // Step 3: Preferences Page := HTMLPages1.Pages.Add; Page.PageName := 'step3'; Page.HTMLContent.Text := GetStyles + '<div class="wizard">' + StepHeader(3) + '<div class="body">' + ' <h2>Preferences</h2>' + ' <div class="field">' + ' <label>Preferred Language</label>' + ' <select name="language" id="language">' + ' <option value="en">English</option>' + ' <option value="fr">French</option>' + ' <option value="de">German</option>' + ' <option value="es">Spanish</option>' + ' </select>' + ' </div>' + ' <div class="field">' + ' <input type="checkbox" name="newsletter" id="newsletter">' + ' <label style="display: inline; font-weight: normal;">Subscribe to newsletter</label>' + ' </div>' + '</div>' + '<div class="buttons">' + ' <button class="btn btn-back" onclick="Wizard:GoBack(''step3'')">Back</button>' + ' <button class="btn btn-next" onclick="Wizard:GoNext(''step3'')">Next</button>' + '</div>' + '</div>';โ // Step 4: Confirmation Page := HTMLPages1.Pages.Add; Page.PageName := 'step4'; Page.HTMLContent.Text := GetStyles + '<div class="wizard">' + StepHeader(4) + '<div class="body">' + ' <h2>Confirm Your Details</h2>' + ' <div id="summaryContent">' + ' <div class="summary-row"><span class="summary-label">Name:</span> <span id="sumName">-</span></div>' + ' <div class="summary-row"><span class="summary-label">Email:</span> <span id="sumEmail">-</span></div>' + ' <div class="summary-row"><span class="summary-label">Street:</span> <span id="sumStreet">-</span></div>' + ' <div class="summary-row"><span class="summary-label">City:</span> <span id="sumCity">-</span></div>' + ' <div class="summary-row"><span class="summary-label">Zip:</span> <span id="sumZip">-</span></div>' + ' <div class="summary-row"><span class="summary-label">Language:</span> <span id="sumLang">-</span></div>' + ' <div class="summary-row"><span class="summary-label">Newsletter:</span> <span id="sumNews">-</span></div>' + ' </div>' + '</div>' + '<div class="buttons">' + ' <button class="btn btn-back" onclick="Wizard:GoBack(''step4'')">Back</button>' + ' <form name="wizardSubmit" style="display:inline; float:right;">' + ' <button type="submit" class="btn btn-submit">Submit</button>' + ' </form>' + '</div>' + '</div>';end;โfunction TfrmWizard.ValidateStep(const StepName: string): Boolean;begin Result := True;โ if StepName = 'step1' then begin var Name := HTMLRender1.GetElementValue('name'); var Email := HTMLRender1.GetElementValue('email');โ if Name.Trim = '' then begin HTMLRender1.SetElementVisible('nameError', True); Result := False; end else HTMLRender1.SetElementVisible('nameError', False);โ if (Email.Trim = '') or (not Email.Contains('@')) then begin HTMLRender1.SetElementVisible('emailError', True); Result := False; end else HTMLRender1.SetElementVisible('emailError', False);โ if Result then begin FData.Values['name'] := Name; FData.Values['email'] := Email; end; endโ else if StepName = 'step2' then begin var Street := HTMLRender1.GetElementValue('street'); var City := HTMLRender1.GetElementValue('city'); var Zip := HTMLRender1.GetElementValue('zip');โ if Street.Trim = '' then begin HTMLRender1.SetElementVisible('streetError', True); Result := False; end else HTMLRender1.SetElementVisible('streetError', False);โ if City.Trim = '' then begin HTMLRender1.SetElementVisible('cityError', True); Result := False; end else HTMLRender1.SetElementVisible('cityError', False);โ if Zip.Trim = '' then begin HTMLRender1.SetElementVisible('zipError', True); Result := False; end else HTMLRender1.SetElementVisible('zipError', False);โ if Result then begin FData.Values['street'] := Street; FData.Values['city'] := City; FData.Values['zip'] := Zip; end; endโ else if StepName = 'step3' then begin FData.Values['language'] := HTMLRender1.GetElementValue('language'); FData.Values['newsletter'] := HTMLRender1.GetElementValue('newsletter'); end;end;โprocedure TfrmWizard.GoNext(FromStep: String);begin if not ValidateStep(FromStep) then Exit;โ if FromStep = 'step1' then HTMLPages1.NavigateTo('step2') else if FromStep = 'step2' then HTMLPages1.NavigateTo('step3') else if FromStep = 'step3' then begin HTMLPages1.NavigateTo('step4'); // Populate summary after navigation renders HTMLRender1.SetElementText('sumName', FData.Values['name']); HTMLRender1.SetElementText('sumEmail', FData.Values['email']); HTMLRender1.SetElementText('sumStreet', FData.Values['street']); HTMLRender1.SetElementText('sumCity', FData.Values['city']); HTMLRender1.SetElementText('sumZip', FData.Values['zip']); HTMLRender1.SetElementText('sumLang', FData.Values['language']); HTMLRender1.SetElementText('sumNews', FData.Values['newsletter']); end;end;โprocedure TfrmWizard.GoBack(FromStep: String);begin if FromStep = 'step2' then HTMLPages1.NavigateTo('step1') else if FromStep = 'step3' then HTMLPages1.NavigateTo('step2') else if FromStep = 'step4' then HTMLPages1.NavigateTo('step3');end;โprocedure TfrmWizard.HTMLPages1BeforeNavigate(Sender: TObject; const FromPage, ToPage: string; var Allow: Boolean);begin // Allow all navigation (validation is handled in GoNext) Allow := True;end;โprocedure TfrmWizard.HTMLRender1FormSubmit(Sender: TObject; const FormName: string; FormData: TStrings);begin if FormName = 'wizardSubmit' then begin ShowMessage(Format( 'Registration complete!' + sLineBreak + 'Name: %s' + sLineBreak + 'Email: %s' + sLineBreak + 'City: %s', [FData.Values['name'], FData.Values['email'], FData.Values['city']])); end;end;โend.14. Common Gotchas#
Page Name Case Sensitivity#
Symptom: Navigation does not work. The renderer stays on the current page.
Fix: Page names are case-sensitive. #Dashboard does not match a page named dashboard:
// Page defined as:Page.PageName := 'dashboard';โ// This works:<a href="#dashboard">Dashboard</a>โ// This does NOT work:<a href="#Dashboard">Dashboard</a>Circular Navigation in OnBeforeNavigate#
Symptom: Application freezes or stack overflow.
Fix: If your OnBeforeNavigate handler calls NavigateTo, it triggers another OnBeforeNavigate event. Always allow navigation to the redirect target:
procedure TForm1.BeforeNavigate(Sender: TObject; const FromPage, ToPage: string; var Allow: Boolean);begin // ALWAYS allow navigation to the login page if ToPage = 'login' then begin Allow := True; Exit; end;โ if not FIsLoggedIn then begin Allow := False; HTMLPages1.NavigateTo('login'); // This triggers BeforeNavigate again // Without the 'login' check above, you get infinite recursion end;end;TwigContent vs HTMLContent Priority#
Symptom: HTML changes to HTMLContent have no effect.
Fix: If TwigContent is not empty, it takes priority and HTMLContent is ignored. Clear TwigContent if you want to use raw HTML:
// If TwigContent has content, HTMLContent is ignoredPage.TwigContent.Clear;Page.HTMLContent.Text := '<h1>This will now display</h1>';Styles Not Persisting Between Pages#
Symptom: CSS styles defined on one page do not apply on another page.
Fix: Each page's HTML is self-contained. Styles defined in one page's <style> block do not carry over when you navigate to another page. Include shared styles in every page, or use a helper function:
function GetSharedStyles: string;begin Result := '<style>/* shared styles */</style>';end;โ// Use in every pagePage.HTMLContent.Text := GetSharedStyles + '<div>Page content</div>';Form Data Lost on Navigation#
Symptom: User fills in a form, navigates away, comes back, and the form is empty.
Fix: Page content is re-rendered from HTMLContent on every navigation. Form values are not preserved. Save form data to variables before navigating away (as shown in the wizard example), or store values in a TStringList and pre-populate fields after navigation.
Summary#
| What | How |
|---|---|
| Setup | HTMLPages1.Renderer := HTMLRender1 |
| Add page (design) | Double-click component, use collection editor |
| Add page (runtime) | HTMLPages1.Pages.Add -- set PageName, HTMLContent |
| Default page | Page.IsDefault := True |
| Navigate via link | <a href="#pagename"> |
| Navigate via code | HTMLPages1.NavigateTo('pagename') |
| Read active page | HTMLPages1.ActivePage |
| Guard navigation | OnBeforeNavigate -- set Allow := False to block |
| Post-navigation | OnAfterNavigate -- load data, update UI |
| Twig in pages | Set TwigContent + SetTwigVariable |
| File templates | Set TwigTemplatePath for {% include %}/{% extends %} | | Page priority | TwigContent (if set) overrides HTMLContent |