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:
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:
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
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:
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:
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:
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#
<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#
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#
<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)#
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#
| Category | Properties |
|---|---|
| Box model | margin, padding, border, border-radius, width, height, min-width, max-width, min-height, max-height, box-sizing, box-shadow |
| Display | block, inline, inline-block, none, table, table-row, table-cell, list-item |
| Text | color, font-size, font-family, font-weight, font-style, text-align, line-height, text-decoration, text-transform, letter-spacing, text-indent, text-overflow, white-space |
| Background | background-color, opacity |
| Visibility | visibility, 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.
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:
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:
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):
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:
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#
| Event | Signature | When |
|---|---|---|
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:
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:
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#
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:
| Expression | Resolves To |
|---|---|
'literal' or "literal" | String literal |
123 | Numeric literal |
this.value | Value of the clicked element |
this.id | ID of the clicked element |
document.getElementById('id').value | Value of element by ID |
document.getElementById('id').<attr> | Any attribute of element by ID |
Example with dynamic values:
Tina4HTMLRender1.HTML.Text := '<input type="text" id="nameInput" placeholder="Type your name">' + '<button onclick="App:Greet(document.getElementById(''nameInput'').value)">' + ' Greet</button>';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#
// Set a form input's valueTina4HTMLRender1.SetElementValue('emailInput', 'user@example.com');โ// Read a form input's valuevar Email := Tina4HTMLRender1.GetElementValue('emailInput');Enable/Disable Controls#
// Disable the submit button until the form is validTina4HTMLRender1.SetElementEnabled('submitBtn', False);โ// Enable it when validation passesTina4HTMLRender1.SetElementEnabled('submitBtn', True);Show/Hide Elements#
// Show an error messageTina4HTMLRender1.SetElementVisible('errorMsg', True);โ// Hide the loading spinnerTina4HTMLRender1.SetElementVisible('spinner', False);Change Text Content#
Tina4HTMLRender1.SetElementText('statusLabel', 'Processing...');Tina4HTMLRender1.SetElementText('recordCount', IntToStr(Count) + ' records');Change Styles#
Tina4HTMLRender1.SetElementStyle('statusLabel', 'color', 'green');Tina4HTMLRender1.SetElementStyle('alertBox', 'background-color', '#fee2e2');Tina4HTMLRender1.SetElementStyle('alertBox', 'border', '1px solid #ef4444');Set Attributes#
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#
// After multiple DOM changes, force a full re-layoutTina4HTMLRender1.RefreshElement('mainContent');DOM Method Reference#
| Method | Description |
|---|---|
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.
// 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.
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.
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#
- Drop a
TTina4HTMLRenderon a form - Create an HTML form with: name (text), email (email), subject (select dropdown), message (textarea)
- Add validation: all fields required, email must contain
@, message minimum 10 characters - Show validation errors inline (red text below each field)
- On successful validation, POST the form data to
/contactas JSON - Show a success message after submission
Solution#
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:
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):
// RegistrationHTMLRender1.RegisterObject('MyApp', Self);โ// HTML must match the registered nameonclick="MyApp:DoSomething('param')" // Correctonclick="Form1:DoSomething('param')" // Wrong name -- will not find the objectForm 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:
<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:
// WRONG -- compilation errorHTML.Text := '<button onclick="App:Do('param')">Click</button>';โ// CORRECT -- doubled single quotesHTML.Text := '<button onclick="App:Do(''param'')">Click</button>';Summary#
| What | How |
|---|---|
| Basic rendering | HTMLRender1.HTML.Text := '<h1>Hello</h1>' |
| External CSS | <link rel="stylesheet" href="..."> |
| Style blocks | <style>.card { ... }</style> |
| Inline styles | style="color: blue;" |
| CSS variables | var(--primary) with :root |
| Form controls | <input>, <select>, <textarea>, <button> |
| Form submit | OnFormSubmit event -- FormData.Values['name'] |
| RTTI onclick | onclick="ObjName:Method(params)" + RegisterObject |
| DOM: set value | SetElementValue('id', 'value') |
| DOM: show/hide | SetElementVisible('id', True/False) |
| DOM: enable | SetElementEnabled('id', True/False) |
| DOM: text | SetElementText('id', 'text') |
| DOM: style | SetElementStyle('id', 'prop', 'value') |
| Image caching | CacheEnabled := True + CacheDir := '...' |