Apg-Tng
SSR Html template engine

ExamplesDeliverablesCache


Documentation

Introduction

This template engine is an evolution of the Drash service named tengine.

Tengine is based on a module named Jae that was originally inspired by a blog post by Krasmir Tsonev: a javascript template engine in 20 lines

How it works

The templates are standard HTML files with additional specific markup called partials. A parser analyzes the HTML files, identifies the specific markup, and builds an anonymous javascript function on the fly. This function will be used to render the actual HTML that will be served back to the client.

ApgTngService has several optimizations to speed up the generation process. Three different caches (that can be enabled optionally), are used to store in memory the HTML templates and the generated functions.

Specific markup

The markup inside HTML files used by ApgTngService is identified by customizable markup delimiters. The default ones are [: ... :]

Inside the delimiters, it is possible to write standard javascript code. The following reserved words and symbols are used to help the engine to identify javascript statements.

function, let, const, =, if, else, switch, case, break, for, do, while, ;, {, }.

If none of the preceding is found inside the delimiters the engine supposes that the statement is a data value that has to be interpolated inside the HTML output.

To customize the behaviour of a template it is possible to pass arguments. Those ones are defined by placeholders identified by a number eg.: [#n].

Data interpolation

It is possible to insert values dynamically inside the templates using a data object. The interpolator identifies the fields of the data object and replaces the placeholders in the template with the appropriate values

Having this data object for a simple menu:


      const templateData = { 
        _links_: [{ 
            href: "/home", 
            caption: "Home",
            color: "red"
          },
          {
            href: "/customers",
            caption: "Customers",
            color: "blue"
          }
          { 
            href: "/orders", 
            caption: "Orders",
            color: "green"
          }]
      }
    

It is possible to write a simple template that creates all the links dynamically


        [: for (const link of _links_ ) { :]
        <p>
          <a href="[: link.href :]" style="color: [: link.color :]">
            [ link.caption ]
          </a>
        </p>
        [: } :]
      

Please note that in the previous template we are using directly the field names of the templateData object. We use as a convention to surroud the fields with the "_" symbol eg: _links_ but it is not mandatory. Is optional to allow to quickly identify fields coming from the data object. The builder of the javascript function wraps all the code inside a "with" statement that simplifies the access to the fields.

Interpolator uses backtick strings and ECMAScript's literals string interpolation, so if specific formatting of the data is required it is necessary to preformat the data as a string, before inserting it in the data object.

Template markup

To combine, merge and embed template files are available the following markup commands:

The "template" argument is the relative path of the HTML file that will be used.

Please note that the relative path can refer to a local folder or to a remote web server. In the latter case, the file is fetched as a deliverable portion. Read ahead for further details.

The root for the relative paths for the local and remote portions are set up with the ApgTngService.Init(...) method. If not specified the "./srv/templates" path is used for the local portions.

The [arguments] argument is optional and is an array of object references that will be used in the partial to replace the placeholdes. This mechanism allows to reuse the same partial in a page with different parameters. Read ahead for further details.

Initialization

To configure the ApgTngService it is necessary to call the Init method that has the following signature.

Init(alocalsPath: string, aremotesUrl: string, aoptions?: IApgTngServiceOptions)

The alocalsPath argument defines the root path referred to Deno.cwd() where the local templates are stored.

The aremotesUrl argument defines the URL of a web server that can deliver HTML templates and portions as text files. See the deliverables section ahead.

The options object has the following fields:

Cache management

The cache must be enabled explicitly setting the useCache: boolean field of the options argument in the Init method.

It is suggested to disable caches in development. Doing so, every time a page will be rendered, the function generation process will be repeated (with additional processing overhead), but will be possible to edit the HTML files on the fly.
After having modified the templates it will be possible to see the changes immediately in the browser by refreshing the page without restarting Deno.
The behavior of this option can be overridden by using the auseCache argument in the ApgTngService.Render() method. Refer to the "Usage" section.

The cache is composed of three subsystems:

The first one is a Map() used to store the HTML templates instead of reading them again and again from the disk or from the internet.
This is almost not very useful, except for debugging purposes. The key of the map is the file name with its relative path.

The second one is a Map() that stores the actual javascript functions generated by the service to render the templates. So once the function is created no further access to the local or remote storage is required.
The key of the map is the file name with its relative path.

The third one is another Map() that contains HTML chunks gathered during the javascript function creation. The chunk is cached if the number of characters is larger than the limit set with the initialization options.
The key of the map is a hash of the chunk content obtained with a Bryc hashing algorithm. This allows to share portions of the pages and reduce the amount of memory used.

Important! The three cache systems will store the data even if the useCache options flag is false. Those caches are constantly updated, so it is possible to browse their content for debugging purposes even if they are not used by the ApgTngService.Render() method.

See the cache management pages to explore the status of the three caches associated with this website.

Usage

Once initialized the service it is possible to invoke the page generation with the call to the Render method that has the following signature:

ApgTngService.Render( atemplateFile: string, atemplateData: unknown, auseCache = true ): string;

The Render method can be used to render an entire page or a single portion, see examples for more details.

The result is the interpolated HTML page or portion as a string that can be sent back to the client in response to an incoming request.

Deliverables

To promote the reuse of portions a web server can be used to store a library of shared components to be delivered as small chunks of HTML with specific markup.
The deliverables usually are specific to a CSS framework, so it is suggested practice to store them in an appropriate series of separate folders.

To get those remote potions, the page developer could specify the full URL to reach them on the remote server:


      [ partial("https://apg-tng.deno.dev/deliver/[cssFramework]/[partialName].html") ]
    

This syntax would couple tightly the page to the remote site that delivers the partials. To get more flexibility a remote host can be specified as an optional argument of the Init method of the ApgTngService: see initialization.

It is possible to refer to remote deliverable portions in the markup using the {Host} syntax:


      [ partial("{Host}/deliver/[cssFramework]/[partialName].html")]
    
Alternatively to get the remote deliverable portion it is possible to use the Host property directly in typescript code using the following syntax.

      const remoteToolbarPartial = ApgTngService.Host + "/deliver/[cssFramework]/[partialName].html";
      const toolBarData = {...}
      const toolbarHtml = await ApgTngService.Render(remoteToolbarPartial, toolBarData) as string;
    

Important. If the cache management is enabled the remote portions are downloaded once and then are reused by the ApgTngService for all the following calls.

ApgTngService can be paired with an ApgTngServer that defines Drash resources to deliver portions to remote clients.
See the Deliverable framework pages to explore the portions delivered by this website.

Components

Data validation before rendering

To strengthen the use of the partials and deliverables, they can be paired with a JSON schema of the template data expected. This allows to validate the data before rendering.

Moreover having a JSON schema defined for the deliverable it is possible to call the ApgTngService.RemoteRender() method.

To define a JSON schema for the partial it is possible to use the following syntax:


      <pre>
        [: schema({
          "$schema": "http://json-schema.org/draft-04/schema#",
          "type": "object",
          "properties": {
            ...
          },
          "required": [
            ...
          ]
        }) :]
      </pre>
    

To preserve formatting it is recommended to wrap the code inside <pre hidden>...</pre> tags.

Usage examples

To help the usage of a deliveralbe it is possible to add a JSON data object to use as an example and show the result with the ApgTngService.RenderExample() method.

To define an example for the partial it is possible to use the following syntax:


      <pre>
        [: example({
          "links" : [
              { "href": "", "caption":"", "description":"" },
              { "href": "", "caption":"", "description":"" },
              { "href": "", "caption":""}
            ]
          }) :]
      </pre>
    

Partial arguments

To help the reuse of partials and deliverables it is possible to define arguments as an array of object names instead than bind the template contant directly with the data object properties.

The partial template will contain numbered placeholders corresponding to the indexes of the array. So having the following partial declaration [: partial("...", [a,b,c]):], the placeholders in the template [#0], [#1], [#2] will refer respectively to the properties a, b, c of the template data object.

Combining JSON schema validation with examples and arguments it is possible to strenghten the behaviour of a partial and consider it as a reusable component.

To define a type signature to the arguments it is possible to use the following syntax:


      <pre>
        [: arguments({
          "#0": {
            "title": "string",
            "links": [
              {
              "href": "string",
              "caption": "string"
              }
            ]
          }
        }) :]
      </pre>