WebStencils
RAD Studio version 12.2 introduces WebStencils, a server-side script-based integration, and processing of HTML files to the WebBroker and RAD Server technologies. This flexible feature empowers you to develop modern websites based on any JavaScript library, powered by the data extracted and processed by a RAD Studio server-side application. It gives you the freedom to create websites that suit your unique needs.
WebStencils' main goal is to help with navigational websites by adopting web technologies (WebBroker, DataSnap, RAD Server) and providing server-side scripting. For instance, you can use WebStencils to generate HTML pages with standard tools and adopt any CSS and JavaScript libraries while retaining the ability to add data from the generated pages coming from the application, like the result of a database query.
In addition, WebStencils can be a good foundation for HTMX as a web development solution. HTMX pages benefit from server-side code generation and hook into REST servers for content updates. Delphi web technologies offer page generation and REST APIs at a very high quality level.
Contents
WebStencil Syntax
The WebStencils scripting language in RAD Studio has a rather simple syntax based on two elements:
- The
@
- Curly braces
{ }
Using the @
Symbol
WebStencils uses the @
symbol as a special marker rather than HTML/XML tags or other notations. The @
symbol is followed by:
- The name of an object or field.
- A special processing keyword.
- Another
@
symbol.
The general value access is based on a dot notation as object.value, so you'll see tags like:
@object.value
The object's name is a symbolic local name that can match an actual server application object assigned with a registration process to the script execution. It can also be resolved in code while processing an OnGetValue event handler.
The value consists of the name of the property (for a generic object), the name of a field (if the object inherits from TDataSet), and a string that is passed in the OnGetValue event handler associated with the processor.
@\value
.Using Curly Braces for Blocks {
}
The second notation uses braces, {
and }, to denote conditional or repeated blocks.
Any other use of curly braces in the source code file (the HTML file) is ignored. In other words, the braces are processed only if used after a specific WebStencils conditional statement.
Accessing Values with the Dot Notation
Take the following HTML WebStencils snippet as an example:
<h2>GetValue</h2>
<p>Value obtained from data: @data.name</p>
<p>Value obtained with request @@data.value: @data.value</p>
The @@
symbols are skipped (we expect a single @
in the output), and the single ones trigger processing.
Also, notice that both symbols, the object name, and the value name, must be valid Delphi identifiers and cannot include spaces or start with numbers. Using underscores and numbers as part of the same name is acceptable.
OnValue Event Handler
It is possible to provide data using a component with the OnValue event handler. For example:
procedure TWebModule13.WebStencilsValueFromEventValue(Sender: TObject;
const ObjectName, FieldName: string; var ReplaceText: string;
var Handled: Boolean)
begin
if SameText (ObjectName, 'data') then
begin
Handled := True;
ReplaceText := 'You requested ' + FieldName;
end;
end;
In the code sample above, the code checks only the object name (the first part of the symbol before the dot), but it is recommended that you also check the FieldName (the symbol after the dot).
Using an Object in the Script Variables Dictionary
The alternative approach is to provide the values using an object. We must configure and associate the object with the action's producer in this case.
This is a complete example, assuming a TMyObject class with a Name string property:
procedure TWebModule1.WebModule1WebActionItem2Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
WebStencilsProcessor1.InputLines.Text := '''
<h2>GetValue</h2>
<p>Value obtained from data: @world.Name</p>
''';
var MyObj := TMyObject.Create();
MyObj.Name := 'Hello hello';
WebStencilsProcessor1.AddVar('world', MyObj, True);
Response.Content := WebStencilsProcessor1.Content;
Handled := True;
end;
The example above results in:
"Value obtained from data: Hello Hello"
Notice that the object is registered with the name 'world', which will later be used in the WebStencils script.
Expressions
The following are the expressions supported:
@ (<expression>)
- variable markers. The expression must be enclosed in parentheses.@if (<expression>)
- "if" command. The expression must be enclosed in parentheses.@Import (<expression>)
,@LayoutPage (<expression>)
,@AddPage (<expression>)
- file" commands. The expression must be enclosed into braces.
The following example shows only the differences from the options above:
WebStencilsProcessor1.InputLines.Text := '''
<h2>GetExpression</h2>
<p>Value obtained from data: @(world.value*2)</p>
''';
MyObj.Value := 2;
The result in the output is:
"Value obtained from data: 4"
In addition to simple expressions, notice you can extend the system with custom function calls. Here is a code sample that registers a “Concat” function receiving two string parameters:
uses
System.Bindings.EvalProtocol, System.Bindings.Methods;
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
MakeInvokable(function(Args: TArray<IValue>): IValue
begin
Result := TValueWrapper.Create(
Args[0].GetValue.AsString + Args[1].GetValue.AsString);
end),
'Concat', 'Concat', '', True, '', nil));
In a WebStencils script, you can use it in an expression like:
@(Concat('aaa', 'bbb']]
Using a DataSet
The alternative option is to use the current record of a dataset rather than an object. In this case, your code might look as follows:
procedure TWebModule13.WebModule13waCompanyAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse;
var Handled: Boolean];
begin
ClientDataSet1.Open;
if not WebStencilsValueFromObject.InVars ('dataset'] then
WebStencilsValueFromObject.AddVar(
'dataset', ClientDataSet1, False]; // False = do not destroy
Response.Content := WebStencilsValueFromObject.Content;
ClientDataSet1.Close;
end;
The matching HTML can have references to some of the fields of the record a sample output is below:
<div class="list-group w-50">
@foreach dataset {
<a href="/[email protected]" class="list-group-item
list-group-item-action" aria-current="true">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">@loop.company</h5>
<small>@loop.country</small>
</div>
<p class="mb-1">@loop.city</p>
<small>@loop.state</small>
</a>
}
Using Modules Variables
Alternatively, to register a specific object for a WebStencilsProcessor, you can add a “module” with multiple objects. A 'module' in this context refers to a collection of related objects or functions. Any field or property of the module can be decorated with the [WebStencilsVar]
attribute, which allows you to specify how the WebStencilsProcessor should process the data.
For example, suppose your data module has an FDMemTable1 component of type TFDMemTable marked as [WebStencilsVar]
, and with a LastName field, you can write:
WebStencilsProcessor1.InputLines.Text := '''
<h2>GetTableData</h2>
<p>Value obtained from table: @(FDMemTable1.LastName)</p>
''';
WebStencilsProcessor1.AddModule(self);
The example above returns the LastName value for the first record. Use a @for
loop to display all the records.
WebStencil Keywords
The WebStencils engine uses the following special tags as keywords (note that these names cannot be used for script variables).
Keyword | Description |
---|---|
@query
|
Use to read HTTP query parameters. |
@page
|
It offers access to the page name and URL arguments. See the PathTemplate property for more information. |
@lang
|
Use for translation support, which triggers a separate OnLang event rather than the OnValue event. |
@* .. *@
|
Use to add comments in the script, which get omitted from the resulting HTML. |
@if object.value { … } [@else { … }]
|
Executes the following block among braces only if the value is true .
|
@if (<expression>)
|
|
@if not object.value { ... } [@else { … }]
|
Executes the following block among braces only if the value is not true . For example: @if obj1.ValueBelowTen {
<p>@obj1.name <span> has a value of </span> @obj1.value</p>
}
|
@ForEach (var object in list) { … }
|
Executes the following block among braces as many times as there are elements in the enumerator. Currently supports:
|
@ForEach object { … }
|
Executes the following block among braces as many times as there are elements in the enumerator. Currently supports Datasets and TObjectList, which must be added as script variables. |
@loop
|
Use inside a block to refer to the current element of the enumeration (list or dataset). Followed by the name of a property or field without using a name for the objects. |
@Import
|
allows you to merge an external file into the specific location of the external file where the filename assumes a .HTML extension. The format is: @import filename [ {<alias1> = <var1> [..., <aliasN> = <varN>]} ]
|
@Scaffolding
|
Placeholder for scaffolding. Mechanism to dynamically generate HTML based on some application data structure (the properties of an object and the database fields). The server-side generated HTML can include other tags to access data. The following is an example of a simple HTML layout generated in the OnScaffolding event handle for each field in a database: procedure TWebModule1.ProcessorCompanyScaffolding(Sender: TObject;
const AQualifClassName: string; var AReplaceText: string);
begin
if SameText (AQualifClassName, 'Company') then
begin
AReplaceText := '';
for var I := 0 to ClientDataSet1.Fields.Count -1 do
AReplaceText := AReplaceText + '<p>'+
ClientDataSet1.Fields[I].FieldName +
': @dataset.' + ClientDataSet1.Fields[I].FieldName + '</p>';
end;
end;
|
@LoginRequired
|
Halts execution if login is required and the user is not logged in. |
@LoginRequired (<role>)
|
The engine checks that the user is logged in but also that the user has the specific role active, gating the page by permission. For example: @LoginRequired (admin) |
Import aliases
The example below shows how to use import aliases.
@import one {@o1 = @object, @d1 = @data} Hello
In this code, @object and @data are the real variables in the executed script. The aliases @o1 and @d1 are script variables for the "one" script and are accessible in this script as normal variables. This way, you can pass to QA script variables with different names, providing the name the script expects as an alias.
WebStencils Templates: Layout and Body
A must-have feature of a template engine is the ability to merge a shared HTML template with the actual content of a page. This is accomplished (like in the .NET version) using four special symbols:
- @LayoutPage filename: This command is used in a specific HTML page (generally at the top) to indicate which template file to use as the structure for the specific page. Multiple pages can share the same template, but not all pages in a project (even using TWebStencilsEngine) or a virtual folder need to use the same template.
- Note: Notice you should use this only once on a page. If you attempt to use more than one @LayoutPage, it is blocked by an exception.
- @RenderBody: This command (with no parameters) is a placeholder in a template file that indicates where to place the actual content of a specific page.
The following is an example of how to use @LayoutPage and @RenderBody:
Test.html
@LayoutPage BaseTemplate
<h2>Page Test3</h2>
<p>Local data param one is @page.name (the page name)</p>
BaseTemplate.html
<html lang="en">
<head>
<!-- Bootstrap CSS →
<link href="https://.../bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-md bg-dark mb-4">...
<main class="container">
<div class="bg-light p-5 rounded">
@RenderBody
</div>
</main>
</body>
</html>
In addition, In addition, you can inject some extra headers from the specific document into the template (for example, a required JavaScript file to include in the page or a page-specific additional CSS). In this case, the mechanism is reversed:
- @ExtraHeader { ... }: Used within the specific HTML page to indicate a block of code to be added as extra header information.
- @RenderHeader: Indicates in the template file (generally in the HTML head section) where to add the extra header of the page, if available
WebStencils Components
The WebStencils package installs two different components: a single file processor and an engine that can instantiate individual processors and provide global configuration for either the stand-alone processors or those manually connected to them.
This core component can take an HTML file, embed the WebStencils notation, and convert it to plain HTML after processing the @
tags.
The TWebStencilsProcessor class processes an individual file (generally with an HTML extension) and its associated template, if any. The processor may be used standalone and assigned to TWebActionItem.Producer, or created and called by TWebStencilsEngine as a post-processor of text files returned by a file dispatcher.
WebStencilsEngine Component
The engine component can be used in two scenarios:
- Connecting it to one or more WebStencilsProcessor components to provide shared settings and behavior.
- Creating WebStencilsProcessor components when needed and placing only this component on the Web modules.
Using WebStencils with RAD Server
The WebStencils template library can also be used in a RAD Server application. It is recommended that the existing TEMSFileResource' component be combined with a TWebStencilsEngine component. The first maps the file system, while the second manages the HTTP mapping and the template processing. The components are connected using the Dispatcher property of the WebStencil Engine.
The example below shows the configuration to add to a RAD Server web module two components:
object html: TEMSFileResource
PathTemplate = '..\..\html\{filename}'
end
object WebStencilsEngine1: TWebStencilsEngine
Dispatcher = html
PathTemplates = <
item
Template = '/'
Redirect = '/test1.html'
end
item
Template = '/list'
Redirect = '/companylist.html'
end
item
Template = '/company'
OnPathInit = WebStencilsEngine1PathTemplates2PathInit
end
...>
end
The PathTemplates are used to map incoming URLs to actual files. This is how the configuration looks at design time:
In addition, it is necessary to configure the higher-level mapping in the date module:
type
[ResourceName('testfile')]
TTestfileResource1 = class(TDataModule)
[ResourceSuffix('./')]
[ResourceSuffix('get', './')]
[EndpointProduce('get', 'text/html')]
html: TEMSFileResource;
To map multiple different paths (compared to the one above), add additional TEMSFileResource components. The TWebStencilsEngine component can only have one Dispatcher indicated in the matching property. Associate more dispatchers using the following code:
AddProcessor(html2, WebStencilsEngine1);