Tina4

Chapter 4: HTML Rendering#

A Web Browser Inside Your Desktop App#

Your FMX application needs a dashboard with styled cards, tables, and action buttons. You could build it with native controls -- dozens of TLabel, TRectangle, TPanel, and TLayout components, each positioned and styled by hand. Or you could write HTML.

TTina4HTMLRender is an FMX control that parses HTML and CSS and renders them directly on a canvas. It is not a web browser. It does not embed Chromium. It does not spawn a separate process. It is a native FMX control that understands HTML structure, CSS styling, form controls, and interactive events. Drop it on your form, set the HTML.Text property, and you have a styled, interactive UI in your desktop application.


1. Basic Usage#

Drop a TTina4HTMLRender on your form. Set its Align to Client so it fills the form. Then set the HTML:

pascal
Tina4HTMLRender1.HTML.Text :=  '<h1>Hello from Tina4</h1>' +  '<p>This is <b>bold</b> and <i>italic</i> text rendered on an FMX canvas.</p>' +  '<hr>' +  '<p style="color: blue; font-size: 18px;">Styled paragraph with inline CSS.</p>';

Run the app. You see a rendered heading, a paragraph with bold and italic, a horizontal rule, and a blue styled paragraph. No web view. No Chromium. Just canvas drawing.

Updating Content#

Change the HTML at any time and the control re-renders:

pascal
procedure TForm1.btnRefreshClick(Sender: TObject);begin  Tina4HTMLRender1.HTML.Text :=    '<h1>Updated at ' + FormatDateTime('hh:nn:ss', Now) + '</h1>';end;

2. Supported HTML Elements#

The renderer supports a practical subset of HTML -- everything you need for dashboards, forms, reports, and documentation displays.

Block Elements#

h1 through h6, p, div, pre, blockquote, hr, fieldset

pascal
Tina4HTMLRender1.HTML.Text :=  '<div style="padding: 10px; border: 1px solid #ccc;">' +  '  <h2>Section Title</h2>' +  '  <p>Regular paragraph text.</p>' +  '  <blockquote>A quoted passage with special styling.</blockquote>' +  '  <pre>Preformatted code block</pre>' +  '</div>';

Inline Elements#

span, b/strong, i/em, a, br, small, label, kbd, abbr, cite, q, var, samp, dfn, time

Lists#

ul, ol, li with bullet and number markers. The list-style-type CSS property is supported:

pascal
Tina4HTMLRender1.HTML.Text :=  '<h3>Features</h3>' +  '<ul>' +  '  <li>REST client components</li>' +  '  <li>JSON data binding</li>' +  '  <li>HTML rendering with CSS</li>' +  '</ul>' +  '<h3>Steps</h3>' +  '<ol>' +  '  <li>Install the packages</li>' +  '  <li>Drop components on form</li>' +  '  <li>Set properties and run</li>' +  '</ol>';

Tables#

table, tr, td, th, thead, tbody, tfoot with collapsed borders:

pascal
Tina4HTMLRender1.HTML.Text :=  '<table style="width: 100%; border-collapse: collapse;">' +  '  <thead>' +  '    <tr>' +  '      <th style="border: 1px solid #ddd; padding: 8px; background: #f5f5f5;">Name</th>' +  '      <th style="border: 1px solid #ddd; padding: 8px; background: #f5f5f5;">Email</th>' +  '      <th style="border: 1px solid #ddd; padding: 8px; background: #f5f5f5;">Status</th>' +  '    </tr>' +  '  </thead>' +  '  <tbody>' +  '    <tr>' +  '      <td style="border: 1px solid #ddd; padding: 8px;">Alice</td>' +  '      <td style="border: 1px solid #ddd; padding: 8px;">alice@example.com</td>' +  '      <td style="border: 1px solid #ddd; padding: 8px;">Active</td>' +  '    </tr>' +  '    <tr>' +  '      <td style="border: 1px solid #ddd; padding: 8px;">Bob</td>' +  '      <td style="border: 1px solid #ddd; padding: 8px;">bob@example.com</td>' +  '      <td style="border: 1px solid #ddd; padding: 8px;">Inactive</td>' +  '    </tr>' +  '  </tbody>' +  '</table>';

Images#

img with HTTP download, async loading, and disk-based caching:

pascal
Tina4HTMLRender1.CacheEnabled := True;Tina4HTMLRender1.CacheDir := 'C:\MyApp\cache';Tina4HTMLRender1.HTML.Text :=  '<img src="https://picsum.photos/300/200" width="300" height="200">' +  '<p>Image loaded from the web and cached to disk.</p>';

3. CSS Support#

The renderer supports a substantial CSS feature set -- enough for professional-looking UIs without reaching for native FMX styling.

External Stylesheets#

html
<link rel="stylesheet" href="https://example.com/styles.css">

Stylesheets are downloaded via HTTP and cached. This means you can use external CSS frameworks or shared stylesheets.

Style Blocks#

pascal
Tina4HTMLRender1.HTML.Text :=  '<style>' +  '  .card { border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; margin: 8px; }' +  '  .card h3 { margin: 0 0 8px 0; color: #333; }' +  '  .card p { color: #666; font-size: 14px; }' +  '  .status-active { color: green; font-weight: bold; }' +  '  .status-inactive { color: red; }' +  '</style>' +  '<div class="card">' +  '  <h3>User Dashboard</h3>' +  '  <p>Status: <span class="status-active">Active</span></p>' +  '</div>';

Inline Styles#

html
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 4px;">  <p style="font-size: 16px; color: #333;">Inline styled content.</p></div>

Selector Support#

  • Tag selectors: h1, p, div
  • Class selectors: .card, .btn
  • ID selectors: #header, #main
  • Combined selectors: div.card, p.highlight
  • Specificity-based cascade: more specific selectors override less specific ones

Custom Properties (CSS Variables)#

pascal
Tina4HTMLRender1.HTML.Text :=  '<style>' +  '  :root { --primary: #2563eb; --text: #333; --bg: #f8f9fa; }' +  '  .header { color: var(--primary); background: var(--bg); padding: 16px; }' +  '  .body { color: var(--text); padding: 16px; }' +  '</style>' +  '<div class="header"><h2>Dashboard</h2></div>' +  '<div class="body"><p>Content styled with CSS variables.</p></div>';

Supported CSS Properties#

CategoryProperties
Box modelmargin, padding, border, border-radius, width, height, min-width, max-width, min-height, max-height, box-sizing, box-shadow
Displayblock, inline, inline-block, none, table, table-row, table-cell, list-item
Textcolor, font-size, font-family, font-weight, font-style, text-align, line-height, text-decoration, text-transform, letter-spacing, text-indent, text-overflow, white-space
Backgroundbackground-color, opacity
Visibilityvisibility, overflow, display: none
Bootstrap 5.btn variants, .form-control, .form-check, .text-muted -- fallback styles are built in

4. Form Controls#

HTML form elements create native FMX controls overlaid on the rendered content. These are real editable controls -- text inputs, checkboxes, radio buttons, dropdowns, and buttons.

pascal
Tina4HTMLRender1.HTML.Text :=  '<style>' +  '  .form-group { margin-bottom: 12px; }' +  '  label { display: block; margin-bottom: 4px; font-weight: bold; }' +  '  input, select, textarea { width: 300px; padding: 6px; border: 1px solid #ccc; }' +  '  .btn { padding: 8px 16px; background: #2563eb; color: white; border: none; cursor: pointer; }' +  '</style>' +  '<form name="userForm">' +  '  <div class="form-group">' +  '    <label>Name</label>' +  '    <input type="text" name="username" id="username" placeholder="Enter your name">' +  '  </div>' +  '  <div class="form-group">' +  '    <label>Email</label>' +  '    <input type="email" name="email" id="email" placeholder="you@example.com">' +  '  </div>' +  '  <div class="form-group">' +  '    <label>Password</label>' +  '    <input type="password" name="password" id="password">' +  '  </div>' +  '  <div class="form-group">' +  '    <label>Role</label>' +  '    <select name="role" id="role">' +  '      <option value="user">User</option>' +  '      <option value="admin">Admin</option>' +  '      <option value="editor">Editor</option>' +  '    </select>' +  '  </div>' +  '  <div class="form-group">' +  '    <label>Bio</label>' +  '    <textarea name="bio" id="bio" rows="4"></textarea>' +  '  </div>' +  '  <div class="form-group">' +  '    <input type="checkbox" name="terms" id="terms">' +  '    <label style="display: inline;">I agree to the terms</label>' +  '  </div>' +  '  <button type="submit" class="btn">Submit</button>' +  '</form>';

Supported input types: text, password, email, radio, checkbox, submit, button, reset, file. Plus textarea, select/option, and button.


5. Events#

The renderer fires events for form interactions, element clicks, and link clicks.

OnFormSubmit#

Fires when a submit button is clicked. Collects all form data as name=value pairs:

pascal
procedure TForm1.HTMLRender1FormSubmit(Sender: TObject;  const FormName: string; FormData: TStrings);var  Username, Email, Role: string;begin  Username := FormData.Values['username'];  Email := FormData.Values['email'];  Role := FormData.Values['role'];โ€‹  ShowMessage(Format('Form "%s" submitted. User: %s, Email: %s, Role: %s',    [FormName, Username, Email, Role]));end;

OnFormControlChange#

Fires when any form control's value changes:

pascal
procedure TForm1.HTMLRender1FormControlChange(Sender: TObject;  const Name, Value: string);begin  // React to real-time changes  if Name = 'role' then  begin    if Value = 'admin' then      Tina4HTMLRender1.SetElementVisible('adminPanel', True)    else      Tina4HTMLRender1.SetElementVisible('adminPanel', False);  end;end;

OnFormControlClick#

Fires when a form control is clicked (useful for buttons that are not submit buttons):

pascal
procedure TForm1.HTMLRender1FormControlClick(Sender: TObject;  const Name, Value: string);begin  if Name = 'cancelBtn' then    ClearForm;end;

OnLinkClick#

Fires when an anchor tag is clicked. Set Handled := True to prevent default navigation:

pascal
procedure TForm1.HTMLRender1LinkClick(Sender: TObject;  const AURL: string; var Handled: Boolean);begin  if AURL.StartsWith('http') then  begin    // Open in system browser instead of navigating    ShellExecute(0, 'open', PChar(AURL), nil, nil, SW_SHOWNORMAL);    Handled := True;  end;end;

Event Reference#

EventSignatureWhen
OnFormControlChange(Sender; Name, Value: string)Form control value changes
OnFormControlClick(Sender; Name, Value: string)Form control clicked
OnFormControlEnter(Sender; Name, Value: string)Form control gains focus
OnFormControlExit(Sender; Name, Value: string)Form control loses focus
OnFormSubmit(Sender; FormName: string; FormData: TStrings)Submit button clicked
OnElementClick(Sender; ObjectName, MethodName: string; Params: TStrings)onclick RTTI element clicked
OnLinkClick(Sender; URL: string; var Handled: Boolean)Anchor href clicked

6. onclick and RTTI -- Calling Pascal from HTML#

Any HTML element can call a Pascal method directly using the onclick attribute with a special syntax: onclick="ObjectName:MethodName(params)". This bridges HTML events to Delphi code without writing event handlers.

Step 1: Register Your Object#

In your form's OnCreate, register the Delphi object that will receive calls:

pascal
procedure TForm1.FormCreate(Sender: TObject);begin  Tina4HTMLRender1.RegisterObject('App', Self);end;

Step 2: Write the Target Method#

The method must be published or use {$M+} RTTI. Parameters are passed as strings:

pascal
procedure TForm1.ShowAlert(Message: String);begin  ShowMessage(Message);end;โ€‹procedure TForm1.HandleAction(Action: String; ItemId: String);begin  if Action = 'delete' then    DeleteItem(ItemId)  else if Action = 'edit' then    EditItem(ItemId);end;

Step 3: Call from HTML#

pascal
Tina4HTMLRender1.HTML.Text :=  '<button onclick="App:ShowAlert(''Hello from HTML!'')">Say Hello</button>' +  '<button onclick="App:HandleAction(''edit'', ''42'')">Edit Item 42</button>' +  '<button onclick="App:HandleAction(''delete'', ''42'')">Delete Item 42</button>';

Dynamic Parameter Expressions#

The onclick handler supports dynamic expressions, not just string literals:

ExpressionResolves To
'literal' or "literal"String literal
123Numeric literal
this.valueValue of the clicked element
this.idID of the clicked element
document.getElementById('id').valueValue of element by ID
document.getElementById('id').<attr>Any attribute of element by ID

Example with dynamic values:

pascal
Tina4HTMLRender1.HTML.Text :=  '<input type="text" id="nameInput" placeholder="Type your name">' +  '<button onclick="App:Greet(document.getElementById(''nameInput'').value)">' +  '  Greet</button>';
pascal
procedure TForm1.Greet(Name: String);begin  ShowMessage('Hello, ' + Name + '!');end;

7. DOM Manipulation#

Modify rendered HTML elements from Delphi code at runtime. Update text, change styles, show/hide elements, enable/disable controls -- all without re-rendering the entire HTML.

Get and Set Values#

pascal
// Set a form input's valueTina4HTMLRender1.SetElementValue('emailInput', 'user@example.com');โ€‹// Read a form input's valuevar Email := Tina4HTMLRender1.GetElementValue('emailInput');

Enable/Disable Controls#

pascal
// Disable the submit button until the form is validTina4HTMLRender1.SetElementEnabled('submitBtn', False);โ€‹// Enable it when validation passesTina4HTMLRender1.SetElementEnabled('submitBtn', True);

Show/Hide Elements#

pascal
// Show an error messageTina4HTMLRender1.SetElementVisible('errorMsg', True);โ€‹// Hide the loading spinnerTina4HTMLRender1.SetElementVisible('spinner', False);

Change Text Content#

pascal
Tina4HTMLRender1.SetElementText('statusLabel', 'Processing...');Tina4HTMLRender1.SetElementText('recordCount', IntToStr(Count) + ' records');

Change Styles#

pascal
Tina4HTMLRender1.SetElementStyle('statusLabel', 'color', 'green');Tina4HTMLRender1.SetElementStyle('alertBox', 'background-color', '#fee2e2');Tina4HTMLRender1.SetElementStyle('alertBox', 'border', '1px solid #ef4444');

Set Attributes#

pascal
Tina4HTMLRender1.SetElementAttribute('myImage', 'src', 'https://example.com/new-photo.jpg');Tina4HTMLRender1.SetElementAttribute('myLink', 'href', '/new-page');โ€‹// Changing class or style triggers relayoutTina4HTMLRender1.SetElementAttribute('myDiv', 'class', 'card highlighted');

Force Refresh#

pascal
// After multiple DOM changes, force a full re-layoutTina4HTMLRender1.RefreshElement('mainContent');

DOM Method Reference#

MethodDescription
GetElementById(Id)Returns the THTMLTag for the element
GetElementValue(Id)Gets the live value from a native control or DOM attribute
SetElementValue(Id, Value)Sets the value on native controls and DOM
SetElementAttribute(Id, Attr, Value)Sets any attribute; triggers relayout for class/style
SetElementEnabled(Id, Enabled)Enables/disables native controls
SetElementVisible(Id, Visible)Shows/hides elements via display:none
SetElementText(Id, Text)Updates inner text content
SetElementStyle(Id, Prop, Value)Sets an inline style property
RefreshElement(Id)Forces a full re-layout and repaint

8. Image Loading and Caching#

Images referenced in <img> tags are downloaded asynchronously via HTTP. Once downloaded, they are cached to disk so subsequent renders are instant.

pascal
// Enable caching and set the cache directoryTina4HTMLRender1.CacheEnabled := True;Tina4HTMLRender1.CacheDir := 'C:\MyApp\cache';โ€‹// Images load in the background and appear when readyTina4HTMLRender1.HTML.Text :=  '<div style="display: inline-block; margin: 8px;">' +  '  <img src="https://picsum.photos/200/150" width="200" height="150">' +  '  <p style="text-align: center;">Random photo</p>' +  '</div>';

The first load downloads the image. Subsequent loads read from C:\MyApp\cache. Without a cache directory, images are re-downloaded every time.


9. Complete Example: Login Form with Validation#

A login form with username and password fields, client-side validation, error display, and a submit handler that calls a REST API.

pascal
unit LoginForm;โ€‹interfaceโ€‹uses  System.SysUtils, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls,  Tina4HTMLRender, Tina4REST;โ€‹type  TfrmLogin = class(TForm)    HTMLRender1: TTina4HTMLRender;    restAPI: TTina4REST;    procedure FormCreate(Sender: TObject);    procedure HTMLRender1FormSubmit(Sender: TObject;      const FormName: string; FormData: TStrings);  private    procedure RenderLoginPage;    procedure ShowError(const Msg: string);    procedure ShowSuccess;  published    procedure ForgotPassword(Action: String);  end;โ€‹var  frmLogin: TfrmLogin;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TfrmLogin.FormCreate(Sender: TObject);begin  restAPI.BaseUrl := 'https://api.example.com';  HTMLRender1.RegisterObject('Login', Self);  RenderLoginPage;end;โ€‹procedure TfrmLogin.RenderLoginPage;begin  HTMLRender1.HTML.Text :=    '<style>' +    '  body { font-family: Arial, sans-serif; background: #f5f5f5; }' +    '  .login-card { max-width: 400px; margin: 40px auto; padding: 32px;' +    '    background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }' +    '  .login-card h2 { margin: 0 0 24px 0; text-align: center; color: #333; }' +    '  .form-group { margin-bottom: 16px; }' +    '  .form-group label { display: block; margin-bottom: 4px; font-weight: bold;' +    '    color: #555; font-size: 14px; }' +    '  .form-group input { width: 100%; padding: 10px; border: 1px solid #ddd;' +    '    border-radius: 4px; font-size: 14px; }' +    '  .btn-login { width: 100%; padding: 12px; background: #2563eb; color: white;' +    '    border: none; border-radius: 4px; font-size: 16px; font-weight: bold; }' +    '  .error { background: #fee2e2; color: #dc2626; padding: 10px; border-radius: 4px;' +    '    margin-bottom: 16px; display: none; }' +    '  .success { background: #dcfce7; color: #16a34a; padding: 10px; border-radius: 4px;' +    '    margin-bottom: 16px; display: none; }' +    '  .forgot { text-align: center; margin-top: 16px; }' +    '  .forgot a { color: #2563eb; font-size: 14px; }' +    '</style>' +    '<div class="login-card">' +    '  <h2>Sign In</h2>' +    '  <div class="error" id="errorBox">Error message here</div>' +    '  <div class="success" id="successBox">Login successful!</div>' +    '  <form name="loginForm">' +    '    <div class="form-group">' +    '      <label>Email</label>' +    '      <input type="email" name="email" id="email" placeholder="you@example.com">' +    '    </div>' +    '    <div class="form-group">' +    '      <label>Password</label>' +    '      <input type="password" name="password" id="password" placeholder="Enter password">' +    '    </div>' +    '    <button type="submit" class="btn-login" id="btnSubmit">Sign In</button>' +    '  </form>' +    '  <div class="forgot">' +    '    <span onclick="Login:ForgotPassword(''reset'')" ' +    '      style="color: #2563eb; cursor: pointer;">Forgot your password?</span>' +    '  </div>' +    '</div>';end;โ€‹procedure TfrmLogin.HTMLRender1FormSubmit(Sender: TObject;  const FormName: string; FormData: TStrings);var  Email, Password: string;  StatusCode: Integer;  Response: TJSONObject;begin  if FormName <> 'loginForm' then Exit;โ€‹  Email := FormData.Values['email'];  Password := FormData.Values['password'];โ€‹  // Client-side validation  if Email.Trim = '' then  begin    ShowError('Email is required');    Exit;  end;  if Password.Trim = '' then  begin    ShowError('Password is required');    Exit;  end;  if not Email.Contains('@') then  begin    ShowError('Please enter a valid email address');    Exit;  end;โ€‹  // Disable the button while processing  HTMLRender1.SetElementEnabled('btnSubmit', False);  HTMLRender1.SetElementText('btnSubmit', 'Signing in...');โ€‹  // Call the API  Response := restAPI.Post(StatusCode, '/auth/login', '',    Format('{"email": "%s", "password": "%s"}', [Email, Password]));  try    if StatusCode = 200 then    begin      var Token := Response.GetValue<String>('token');      restAPI.SetBearer(Token);      ShowSuccess;    end    else    begin      ShowError('Invalid email or password');    end;  finally    Response.Free;    HTMLRender1.SetElementEnabled('btnSubmit', True);    HTMLRender1.SetElementText('btnSubmit', 'Sign In');  end;end;โ€‹procedure TfrmLogin.ShowError(const Msg: string);begin  HTMLRender1.SetElementVisible('successBox', False);  HTMLRender1.SetElementText('errorBox', Msg);  HTMLRender1.SetElementVisible('errorBox', True);end;โ€‹procedure TfrmLogin.ShowSuccess;begin  HTMLRender1.SetElementVisible('errorBox', False);  HTMLRender1.SetElementVisible('successBox', True);end;โ€‹procedure TfrmLogin.ForgotPassword(Action: String);begin  ShowMessage('Forgot password flow: ' + Action);end;โ€‹end.

10. Complete Example: Interactive Dashboard#

A dashboard with stats cards, a data table, and action buttons that call Pascal methods. This demonstrates combining styled HTML, dynamic data, and RTTI-based event handling.

pascal
unit Dashboard;โ€‹interfaceโ€‹uses  System.SysUtils, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls,  FireDAC.Comp.Client,  Tina4HTMLRender, Tina4REST, Tina4Core;โ€‹type  TfrmDashboard = class(TForm)    HTMLRender1: TTina4HTMLRender;    restAPI: TTina4REST;    procedure FormCreate(Sender: TObject);  private    procedure RenderDashboard;    function BuildStatsCards: string;    function BuildUserTable: string;  published    procedure ViewUser(UserId: String);    procedure DeleteUser(UserId: String);    procedure RefreshData(Action: String);  end;โ€‹var  frmDashboard: TfrmDashboard;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TfrmDashboard.FormCreate(Sender: TObject);begin  restAPI.BaseUrl := 'https://api.example.com/v1';  HTMLRender1.RegisterObject('Dashboard', Self);  RenderDashboard;end;โ€‹procedure TfrmDashboard.RenderDashboard;begin  HTMLRender1.HTML.Text :=    '<style>' +    '  * { box-sizing: border-box; }' +    '  body { font-family: Arial, sans-serif; padding: 20px; background: #f0f2f5; }' +    '  h1 { color: #1a1a2e; margin-bottom: 24px; }' +    '  .stats { display: inline-block; width: 100%; margin-bottom: 24px; }' +    '  .stat-card { display: inline-block; width: 22%; margin-right: 2%;' +    '    background: white; border-radius: 8px; padding: 20px;' +    '    box-shadow: 0 1px 3px rgba(0,0,0,0.1); }' +    '  .stat-card h3 { margin: 0; color: #888; font-size: 12px; text-transform: uppercase; }' +    '  .stat-card .value { font-size: 28px; font-weight: bold; color: #1a1a2e; margin: 8px 0; }' +    '  .stat-card .change { font-size: 12px; color: #16a34a; }' +    '  table { width: 100%; border-collapse: collapse; background: white;' +    '    border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }' +    '  th { background: #f8f9fa; padding: 12px; text-align: left;' +    '    font-size: 12px; color: #888; text-transform: uppercase; border-bottom: 2px solid #e0e0e0; }' +    '  td { padding: 12px; border-bottom: 1px solid #f0f0f0; color: #333; }' +    '  .btn-sm { padding: 4px 12px; border: none; border-radius: 4px;' +    '    font-size: 12px; cursor: pointer; margin-right: 4px; }' +    '  .btn-view { background: #dbeafe; color: #2563eb; }' +    '  .btn-delete { background: #fee2e2; color: #dc2626; }' +    '  .btn-refresh { padding: 8px 16px; background: #2563eb; color: white;' +    '    border: none; border-radius: 4px; margin-bottom: 16px; }' +    '  .toolbar { margin-bottom: 16px; }' +    '</style>' +    '<h1>Admin Dashboard</h1>' +    '<div class="toolbar">' +    '  <button class="btn-refresh" ' +    '    onclick="Dashboard:RefreshData(''all'')">Refresh Data</button>' +    '</div>' +    BuildStatsCards +    '<h2 style="color: #1a1a2e; margin: 24px 0 16px;">Recent Users</h2>' +    BuildUserTable;end;โ€‹function TfrmDashboard.BuildStatsCards: string;begin  Result :=    '<div class="stats">' +    '  <div class="stat-card">' +    '    <h3>Total Users</h3>' +    '    <div class="value" id="totalUsers">1,234</div>' +    '    <div class="change">+12% this month</div>' +    '  </div>' +    '  <div class="stat-card">' +    '    <h3>Active Sessions</h3>' +    '    <div class="value" id="activeSessions">56</div>' +    '    <div class="change">+3% this hour</div>' +    '  </div>' +    '  <div class="stat-card">' +    '    <h3>Revenue</h3>' +    '    <div class="value" id="revenue">$48,290</div>' +    '    <div class="change">+8% this week</div>' +    '  </div>' +    '  <div class="stat-card">' +    '    <h3>Orders</h3>' +    '    <div class="value" id="orders">389</div>' +    '    <div class="change">+5% today</div>' +    '  </div>' +    '</div>';end;โ€‹function TfrmDashboard.BuildUserTable: string;begin  Result :=    '<table>' +    '  <thead>' +    '    <tr><th>ID</th><th>Name</th><th>Email</th><th>Status</th><th>Actions</th></tr>' +    '  </thead>' +    '  <tbody>' +    '    <tr>' +    '      <td>1</td><td>Alice Smith</td><td>alice@example.com</td>' +    '      <td style="color: green;">Active</td>' +    '      <td>' +    '        <button class="btn-sm btn-view" onclick="Dashboard:ViewUser(''1'')">View</button>' +    '        <button class="btn-sm btn-delete" onclick="Dashboard:DeleteUser(''1'')">Delete</button>' +    '      </td>' +    '    </tr>' +    '    <tr>' +    '      <td>2</td><td>Bob Johnson</td><td>bob@example.com</td>' +    '      <td style="color: green;">Active</td>' +    '      <td>' +    '        <button class="btn-sm btn-view" onclick="Dashboard:ViewUser(''2'')">View</button>' +    '        <button class="btn-sm btn-delete" onclick="Dashboard:DeleteUser(''2'')">Delete</button>' +    '      </td>' +    '    </tr>' +    '    <tr>' +    '      <td>3</td><td>Carol Williams</td><td>carol@example.com</td>' +    '      <td style="color: red;">Inactive</td>' +    '      <td>' +    '        <button class="btn-sm btn-view" onclick="Dashboard:ViewUser(''3'')">View</button>' +    '        <button class="btn-sm btn-delete" onclick="Dashboard:DeleteUser(''3'')">Delete</button>' +    '      </td>' +    '    </tr>' +    '  </tbody>' +    '</table>';end;โ€‹procedure TfrmDashboard.ViewUser(UserId: String);begin  ShowMessage('Viewing user ' + UserId);  // In a real app: navigate to user detail page or show a modalend;โ€‹procedure TfrmDashboard.DeleteUser(UserId: String);begin  ShowMessage('Delete user ' + UserId + '?');  // In a real app: confirm then call DELETE /users/{id}end;โ€‹procedure TfrmDashboard.RefreshData(Action: String);begin  // Refresh stats via DOM manipulation -- no full re-render needed  HTMLRender1.SetElementText('totalUsers', '1,256');  HTMLRender1.SetElementText('activeSessions', '61');  HTMLRender1.SetElementText('revenue', '$49,100');  HTMLRender1.SetElementText('orders', '402');  ShowMessage('Dashboard data refreshed');end;โ€‹end.

11. Exercise: Contact Form#

Build a contact form with name, email, and message fields. Validate all fields before submission. Submit the data to a REST API.

Requirements#

  1. Drop a TTina4HTMLRender on a form
  2. Create an HTML form with: name (text), email (email), subject (select dropdown), message (textarea)
  3. Add validation: all fields required, email must contain @, message minimum 10 characters
  4. Show validation errors inline (red text below each field)
  5. On successful validation, POST the form data to /contact as JSON
  6. Show a success message after submission

Solution#

pascal
unit ContactForm;โ€‹interfaceโ€‹uses  System.SysUtils, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms,  Tina4HTMLRender, Tina4REST;โ€‹type  TfrmContact = class(TForm)    HTMLRender1: TTina4HTMLRender;    restAPI: TTina4REST;    procedure FormCreate(Sender: TObject);    procedure HTMLRender1FormSubmit(Sender: TObject;      const FormName: string; FormData: TStrings);  private    procedure RenderForm;    function Validate(FormData: TStrings): Boolean;  end;โ€‹var  frmContact: TfrmContact;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TfrmContact.FormCreate(Sender: TObject);begin  restAPI.BaseUrl := 'https://api.example.com';  RenderForm;end;โ€‹procedure TfrmContact.RenderForm;begin  HTMLRender1.HTML.Text :=    '<style>' +    '  .container { max-width: 500px; margin: 20px auto; padding: 24px;' +    '    background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }' +    '  h2 { margin: 0 0 20px 0; color: #333; }' +    '  .field { margin-bottom: 16px; }' +    '  .field label { display: block; margin-bottom: 4px; font-weight: bold; font-size: 14px; }' +    '  .field input, .field select, .field textarea ' +    '    { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }' +    '  .field textarea { height: 100px; }' +    '  .error-text { color: #dc2626; font-size: 12px; display: none; margin-top: 4px; }' +    '  .btn-submit { padding: 10px 24px; background: #2563eb; color: white;' +    '    border: none; border-radius: 4px; font-size: 14px; }' +    '  .success-msg { background: #dcfce7; color: #16a34a; padding: 12px;' +    '    border-radius: 4px; display: none; margin-bottom: 16px; }' +    '</style>' +    '<div class="container">' +    '  <h2>Contact Us</h2>' +    '  <div class="success-msg" id="successMsg">Thank you! Your message has been sent.</div>' +    '  <form name="contactForm">' +    '    <div class="field">' +    '      <label>Name</label>' +    '      <input type="text" name="name" id="name" placeholder="Your full 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 class="field">' +    '      <label>Subject</label>' +    '      <select name="subject" id="subject">' +    '        <option value="">Select a subject</option>' +    '        <option value="support">Technical Support</option>' +    '        <option value="sales">Sales Inquiry</option>' +    '        <option value="feedback">Feedback</option>' +    '      </select>' +    '      <div class="error-text" id="subjectError">Please select a subject</div>' +    '    </div>' +    '    <div class="field">' +    '      <label>Message</label>' +    '      <textarea name="message" id="message" placeholder="Your message (min 10 chars)"></textarea>' +    '      <div class="error-text" id="messageError">Message must be at least 10 characters</div>' +    '    </div>' +    '    <button type="submit" class="btn-submit" id="btnSubmit">Send Message</button>' +    '  </form>' +    '</div>';end;โ€‹procedure TfrmContact.HTMLRender1FormSubmit(Sender: TObject;  const FormName: string; FormData: TStrings);var  StatusCode: Integer;  Response: TJSONObject;begin  if FormName <> 'contactForm' then Exit;โ€‹  // Hide previous errors  HTMLRender1.SetElementVisible('nameError', False);  HTMLRender1.SetElementVisible('emailError', False);  HTMLRender1.SetElementVisible('subjectError', False);  HTMLRender1.SetElementVisible('messageError', False);  HTMLRender1.SetElementVisible('successMsg', False);โ€‹  if not Validate(FormData) then Exit;โ€‹  // Submit to API  HTMLRender1.SetElementEnabled('btnSubmit', False);  HTMLRender1.SetElementText('btnSubmit', 'Sending...');โ€‹  Response := restAPI.Post(StatusCode, '/contact', '',    Format('{"name": "%s", "email": "%s", "subject": "%s", "message": "%s"}',      [FormData.Values['name'], FormData.Values['email'],       FormData.Values['subject'], FormData.Values['message']]));  try    if StatusCode in [200, 201] then    begin      HTMLRender1.SetElementVisible('successMsg', True);      // Clear the form      HTMLRender1.SetElementValue('name', '');      HTMLRender1.SetElementValue('email', '');      HTMLRender1.SetElementValue('message', '');    end    else      ShowMessage('Submission failed: HTTP ' + StatusCode.ToString);  finally    Response.Free;    HTMLRender1.SetElementEnabled('btnSubmit', True);    HTMLRender1.SetElementText('btnSubmit', 'Send Message');  end;end;โ€‹function TfrmContact.Validate(FormData: TStrings): Boolean;begin  Result := True;โ€‹  if FormData.Values['name'].Trim = '' then  begin    HTMLRender1.SetElementVisible('nameError', True);    Result := False;  end;โ€‹  var Email := FormData.Values['email'].Trim;  if (Email = '') or (not Email.Contains('@')) then  begin    HTMLRender1.SetElementVisible('emailError', True);    Result := False;  end;โ€‹  if FormData.Values['subject'].Trim = '' then  begin    HTMLRender1.SetElementVisible('subjectError', True);    Result := False;  end;โ€‹  if FormData.Values['message'].Trim.Length < 10 then  begin    HTMLRender1.SetElementVisible('messageError', True);    Result := False;  end;end;โ€‹end.

12. Common Gotchas#

Forgetting to Set Cache Directory for Images#

Symptom: Images load the first time, but every subsequent launch re-downloads them. Or images do not appear at all.

Fix: Set CacheEnabled := True and CacheDir to a writable directory before setting the HTML:

pascal
HTMLRender1.CacheEnabled := True;HTMLRender1.CacheDir := TPath.Combine(TPath.GetDocumentsPath, 'AppCache');ForceDirectories(HTMLRender1.CacheDir);

RTTI Method Not Found#

Symptom: Clicking an onclick element does nothing, or raises an access violation.

Fix: Ensure the target method is published (or the class has {$M+} RTTI). Ensure RegisterObject was called with the correct object name. Ensure the onclick format is exactly ObjectName:MethodName(params):

pascal
// RegistrationHTMLRender1.RegisterObject('MyApp', Self);โ€‹// HTML must match the registered nameonclick="MyApp:DoSomething('param')"  // Correctonclick="Form1:DoSomething('param')"  // Wrong name -- will not find the object

Form Control Name Matching#

Symptom: FormData.Values['username'] returns empty string even though the user typed in the field.

Fix: The name attribute in the HTML must match exactly. Case matters:

html
<input type="text" name="userName">   <!-- FormData.Values['userName'] --><input type="text" name="username">   <!-- FormData.Values['username'] -->

Escaped Quotes in HTML Strings#

Symptom: Compilation error or garbled HTML.

Fix: In Delphi string literals, use doubled single quotes '' for apostrophes inside HTML attributes:

pascal
// WRONG -- compilation errorHTML.Text := '<button onclick="App:Do('param')">Click</button>';โ€‹// CORRECT -- doubled single quotesHTML.Text := '<button onclick="App:Do(''param'')">Click</button>';

Summary#

WhatHow
Basic renderingHTMLRender1.HTML.Text := '<h1>Hello</h1>'
External CSS<link rel="stylesheet" href="...">
Style blocks<style>.card { ... }</style>
Inline stylesstyle="color: blue;"
CSS variablesvar(--primary) with :root
Form controls<input>, <select>, <textarea>, <button>
Form submitOnFormSubmit event -- FormData.Values['name']
RTTI onclickonclick="ObjName:Method(params)" + RegisterObject
DOM: set valueSetElementValue('id', 'value')
DOM: show/hideSetElementVisible('id', True/False)
DOM: enableSetElementEnabled('id', True/False)
DOM: textSetElementText('id', 'text')
DOM: styleSetElementStyle('id', 'prop', 'value')
Image cachingCacheEnabled := True + CacheDir := '...'