WebStencils

From RAD Studio
Jump to: navigation, search

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.

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.

Note: The output of values processed by WebStencils is encoded using the TNetEncoding.HTML.Encode method. You can suppress any encoding by using the syntax @\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:
  • Datasets
  • TObjectList, which must be added to the processor dictionary.
  • Any object with the GetEnumerator method, where the enumerator returns an object value.
@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
Note: Both options are optional.

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:

=link

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);

See Also