Become a member!

Welcome Back TemplatePro

UPDATE (sept 2024): Updated official documentation about TemplatePRO is available here

Back in 2016, I embarked on a small experiment: I started developing a simple template language. At the time, I was going through a “funny phase,” and my intention wasn’t to build a full-fledged template language. It was more of a personal challenge, an exploration with no clear end goal in sight. Fast forward to today, the landscape of web development has significantly evolved. The surge in popularity of HTMX has challenged the long-standing dominance of single-page applications (SPAs) as the only viable approach to building serious web applications. In the world of DMVCFramework, we now have three template language integrations:

While these integrations offer valuable options, I’ve never been completely satisfied with any of them for all use cases within DMVCFramework. None of these solutions fully met the specific needs I envisioned for the template language distributed by default with DMVCFramework, especially when building high-performance, cross-platform web applications.

👉🏼 Just to be clear, current solutions are great (especially the Sempare template language is very powerful) but I want something different for the default DMVCFramework template language, as explained in the next paragraph.

The Case with Existing Template Engines

Each of the existing template engines had strengths, but they also had limitations that kept me searching for something better. Here are some of the requirements that I felt were essential for the ideal DMVCFramework template engine:

  • Permissive Open Source License: A license that allows use in commercial projects without restrictions (e.g., not GPL-only).
  • Cross-Platform Compatibility: It must support Delphi across platforms like Win32, Win64, Linux, and Android.
  • Simple Distribution: The engine should require just a single unit to be added to the project, avoiding dependency bloat.
  • Performance: It should be optimized for performance with native Delphi strings or PChar.
  • Zero External Dependencies: No external dynamic libraries (.dll or .so files) should be required.
  • Delphi Object Support: Direct access to Delphi objects, not just JSON or string data.
  • JSON Objects Compatibility: Built-in support for TJSONObject, not just raw JSON strings.
  • Object Lists as Data Sources: Seamless support for lists of objects, useful when working with MVCActiveRecord.
  • Template Reusability: The ability to reuse templates via includes, partials, etc.
  • Active Development: The template engine must be actively maintained and supported.

Some of these requirements were non-negotiable, while others were more flexible. But the bottom line was clear: I wanted a template engine tailored for DMVCFramework’s needs. After not finding a perfect solution, I realized I had to create it.

Introducing TemplatePro

I’m excited to introduce TemplatePro, a completely rewritten template language designed specifically for DMVCFramework. Starting with dmvcframework-3.4.2-magnesium-rc3, TemplatePro will be included as the default template engine in DMVCFramework. However, just like LoggerPro, TemplatePro is not confined to DMVCFramework projects; it can be used in any Delphi application that requires a robust and efficient templating system.

TemplatePro is built to be:

  • Fast: It features a highly efficient compiler and runtime engine optimized for Delphi applications.
  • Extensible: It allows custom filters and functionality to be added easily.
  • Cross-Platform: It supports all major Delphi targets, including Windows, Linux, and Android.
  • Lightweight: With minimal dependencies, TemplatePro can be integrated into your project without unnecessary bloat.

You can find TemplatePro’s source code and documentation in its own repository here.

Sample Code Using TemplatePro

Below is a practical example demonstrating how to use TemplatePro within a console application. This example shows how to loop through a dataset, use conditional statements, apply filters, and work with JSON objects:

program tproconsole;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, TemplatePro, Data.DB, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf,
  FireDAC.DApt.Intf, FireDAC.Comp.DataSet, FireDAC.Comp.Client, System.Rtti, JsonDataObjects;

function GetPeopleDataset: TDataSet;
var
  lMT: TFDMemTable;
begin
  lMT := TFDMemTable.Create(nil);
  try
    lMT.FieldDefs.Clear;
    lMT.FieldDefs.Add('id', ftInteger);
    lMT.FieldDefs.Add('first_name', ftString, 20);
    lMT.FieldDefs.Add('last_name', ftString, 20);
    lMT.Active := True;
    lMT.AppendRecord([1, 'Daniele', 'Teti']);
    lMT.AppendRecord([2, 'Peter', 'Parker']);
    lMT.AppendRecord([3, 'Bruce', 'Banner']);
    lMT.AppendRecord([4, 'Scott', 'Summers']);
    lMT.AppendRecord([5, 'Sue', 'Storm']);
    lMT.First;
    Result := lMT;
  except
    lMT.Free;
    raise;
  end;
end;

function GetJSON: TJSONObject;
var
  ChildObj: TJsonObject;
begin
  Result := TJsonObject.Create;
  Result.I['foo'] := 123;
  Result.S['bar'] := 'hello';
  ChildObj := Result.A['myarray'].AddObject;
  ChildObj.S['first_name'] := 'Daniele';
  ChildObj.S['last_name'] := 'Teti';
  ChildObj := ChildObj.O['company'];
  ChildObj.S['name'] := 'bit Time Professionals';
  ChildObj.S['country'] := 'ITALY';

  ChildObj := Result.A['myarray'].AddObject;
  ChildObj.S['first_name'] := 'Bruce';
  ChildObj.S['last_name'] := 'Banner';
  ChildObj := ChildObj.O['company'];
  ChildObj.S['name'] := 'Green Power';
  ChildObj.S['country'] := 'CA';
end;

function AddSquareBrackets(const aValue: TValue; const aParameters: TArray<string>): string;
begin
  Result := '[' + aValue.AsString + ']';
end;

procedure Main;
begin
  var lCompiler := TTProCompiler.Create();
  try
    var lTemplate :=

    'Simple variable:                                                           ' + sLineBreak +
    '{{:variable1}}                                                             ' + sLineBreak +
    '                                                                           ' + sLineBreak +
    'Using loops:                                                               ' + sLineBreak +
    '{{for person in people}}                                                 ' + sLineBreak +
    '  - {{:person.id}}) {{:person.first_name}}, {{:person.last_name}}          ' + sLineBreak +
    '{{endfor}}                                                                ' + sLineBreak +
    '                                                                           ' + sLineBreak +
    'Using if statement:                                                        ' + sLineBreak +
    '{{if people }}People dataset contains records{{endif}}                     ' + sLineBreak +
    '{{if !people }}People dataset doesn''t contain records{{endif}}            ' + sLineBreak +
    '                                                                           ' + sLineBreak +
    '                                                                           ' + sLineBreak +
    'Using filters:                                                             ' + sLineBreak +
    'uppercase: {{:variable1|uppercase}}                                        ' + sLineBreak +
    'lowercase: {{:variable1|lowercase}}                                        ' + sLineBreak +
    'lpad     : {{:variable1|lpad,20,"*"}}                                      ' + sLineBreak +
    'rpad     : {{:variable1|rpad,20,"*"}}                                      ' + sLineBreak +
    '                                                                           ' + sLineBreak +
    'Using custom filters:                                                      ' + sLineBreak +
    'brackets : {{:variable1|brackets}}                                         ' + sLineBreak +
    '                                                                           ' + sLineBreak +
    'Using json objects:                                                        ' + sLineBreak +
    '{{:jobj.foo}}                                                              ' + sLineBreak +
    '{{:jobj.bar}}                                                              ' + sLineBreak +
    '{{""|lpad,40,"_"}}                                                         ' + sLineBreak +
    '{{for item in jobj.myarray}}                                             ' + sLineBreak +
    '  - {{:item.first_name}} {{:item.last_name}}                               ' + sLineBreak +
    '    {{:item.company.name}} - {{:item.company.country}}                     ' + sLineBreak +
    '{{endfor}}                                                                ' + sLineBreak;

    var lCompiledTemplate := lCompiler.Compile(lTemplate);
    var lPeopleDataset := GetPeopleDataset;
    try
      var lJObj := GetJSON;
      try
        lCompiledTemplate.AddFilter('brackets', AddSquareBrackets);
        lCompiledTemplate.SetData('variable1', 'Daniele Teti');
        lCompiledTemplate.SetData('people', lPeopleDataset);
        lCompiledTemplate.SetData('jobj', lJObj);
        WriteLn(lCompiledTemplate.Render);
      finally
        lJObj.Free;
      end;
    finally
      lPeopleDataset.Free;
    end;
  finally
    lCompiler.Free;
  end;
end;

begin
  try
    Main;
  except
    on E: Exception do
    begin
      Writeln(E.ClassName, ': ', E.Message);
    end;
  end;
  Writeln;
  Writeln('Press ENTER to continue...');
  ReadLn;
end.

The previous code generated the following output:

Simple variable:
Daniele Teti

Using loops:

  - 1) Daniele, Teti

  - 2) Peter, Parker

  - 3) Bruce, Banner

  - 4) Scott, Summers

  - 5) Sue, Storm


Using if statement:
People dataset contains records



Using filters:
uppercase: DANIELE TETI
lowercase: daniele teti
lpad     : ********Daniele Teti
rpad     : Daniele Teti********

Using custom filters:
brackets : [Daniele Teti]

Using json objects:
123
hello
________________________________________

  - Daniele Teti
    bit Time Professionals - ITALY

  - Bruce Banner
    Green Power - CA

Work in Progress

While TemplatePro is now available and ready for use, today it’s still a work in progress. We’re constantly improving and optimizing the language. Your feedback will be invaluable in shaping the future of TemplatePro, so please don’t hesitate to share your thoughts.

Update (2024-08-31) TemplatePro is stable and can be used in production systems. Check the official documentations.

Current Integrations

Several DMVCFramework samples have already been updated to use TemplatePro:

More examples and detailed documentation will follow soon. As usual, I’ll be writing a comprehensive guide for DMVCFramework PATREON supporters. If you’re interested in learning more about TemplatePro and its integration with DMVCFramework, consider becoming a supporter.

Comments

comments powered by Disqus