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:
-
[: extends("template") :]
Define an ancestor/parent template, typically a layout or master page, for the current page template. -
[: partial("template" , [arguments]) :]
Define a sibling/child template, typically a reusable component inside the current page template. -
[: yield :]
Used inside layout or master page template to define the placeholder where the child template will be inserted.
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:
-
useCache: boolean
Default value is false. Enables the caching systems of the service.
See the dedicated section for further details. -
cacheChunksLongerThan: number
Default value is 100. When fielduseCache
is set to true defines the minimum number of characters of the HTML chunks that will be cached. -
consoleLog: boolean
Default value is false. It is used for debugging purposes and prints on the StdOut stream the internal messages related to the processing events.
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:
- HTML templates
- Javascript functions
- HTML chunks
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;
-
atemplateFile
File with its relative path that has to be used as a template. Can be an entire page or a portion ; -
atemplateData
Data object that has to be used for dynamic field interpolation; -
auseCache
Flag that can be used to disable theuseCache
initialization option. This can be useful to debug and develop some template portions even if the service is using the cache functionality for the others;
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>