Tina4

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:

  1. TTina4HTMLRender (name: HTMLRender1) -- set Align to Client
  2. TTina4HTMLPages (name: HTMLPages1)

Link them:

pascal
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: home
  • IsDefault: True
  • HTMLContent: <h1>Home</h1><p>Welcome to the app.</p><a href="#settings">Go to Settings</a>

Page 2:

  • PageName: settings
  • IsDefault: False
  • HTMLContent: <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:

pascal
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.

pascal
Page.IsDefault := True;

Only one page should have IsDefault = True. If multiple pages have it set, the first one in the collection wins.


The most natural way to navigate is with HTML anchor tags. The href value maps to a page name:

href valueMaps to PageName
#dashboarddashboard
/settingssettings
aboutabout

The renderer intercepts link clicks and delegates to the pages component. The leading # or / is stripped to match the PageName.

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

pascal
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
pascal
procedure TForm1.OnLoginSuccess;begin  HTMLPages1.NavigateTo('dashboard');end;โ€‹procedure TForm1.OnLogout;begin  HTMLPages1.NavigateTo('login');end;

Reading the Active Page#

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

PropertyTypeDescription
PageNamestringUnique name used as navigation target
TwigContentTStringListTwig template source (rendered via TTina4Twig)
HTMLContentTStringListRaw HTML (used when TwigContent is empty)
IsDefaultBooleanIf 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:

PropertyTypeDescription
PagesTTina4PageCollectionCollection of pages (design-time editable)
RendererTTina4HTMLRenderThe HTML renderer that displays the active page
ActivePagestringName 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:

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

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

pascal
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 %}:

pascal
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.

pascal
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.

pascal
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#

  1. Step 1: Personal Info (name, email) -- both required
  2. Step 2: Address (street, city, zip) -- all required
  3. Step 3: Preferences (language dropdown, newsletter checkbox)
  4. Step 4: Confirmation (summary of all entered data, submit button)
  5. "Next" button validates current step before advancing
  6. "Back" button always works (no validation needed)
  7. Step indicator showing current position (e.g., "Step 2 of 4")

Solution#

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

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

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

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

pascal
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#

WhatHow
SetupHTMLPages1.Renderer := HTMLRender1
Add page (design)Double-click component, use collection editor
Add page (runtime)HTMLPages1.Pages.Add -- set PageName, HTMLContent
Default pagePage.IsDefault := True
Navigate via link<a href="#pagename">
Navigate via codeHTMLPages1.NavigateTo('pagename')
Read active pageHTMLPages1.ActivePage
Guard navigationOnBeforeNavigate -- set Allow := False to block
Post-navigationOnAfterNavigate -- load data, update UI
Twig in pagesSet TwigContent + SetTwigVariable

| File templates | Set TwigTemplatePath for {% include %}/{% extends %} | | Page priority | TwigContent (if set) overrides HTMLContent |