HTML Templating Module
(2Q21)
The HTML templating framework makes creating XQuery applications that produce HTML pages more easy.
Introduction
The main goal of the HTML templating framework is a clean separation of concerns. Generating entire pages in XQuery is quick and dirty, but makes maintenance and code sharing difficult. Ideally people should be able to look at the HTML view of an application and modify its look and feel without knowing XQuery. The application logic, written in XQuery, should be kept separate. Likewise, the XQuery developer should only have to deal with the minimal amount of HTML (generated dynamically).
The templating module also handles most of the HTTP processing an application requires. It does so using sophisticated features like automatic parameter injection and type conversion. The goal was to remove repeating code like:
let $query := request:get-parameter("query", ())
let $field := request:get-parameter("field", ())
...
In fact you should not see many calls to the HTTP request or session module inside a templating function. This is all handled by parameter injection.
Working examples for the templating module can be found in the demo application.
Writing the HTML
The templating module is based mainly on conventions. Wherever possible it tries to make a best guess instead of requiring additional code or annotations. This works as long as the conventions used are sufficiently clear.
The input for the templating framework is always a plain HTML file. The module scans
the HTML view for elements with data-template
attributes
following a simple convention and tries to translate them into XQuery
function calls. By using data-*
attributes, the HTML remains sufficiently clean and does
not get messed up with application code. A web designer could take the HTML files and
work on it without being bothered by the extra class names.
To start with the usual "Hello world" example:
<div data-template="demo:hello" data-template-language="de"/>
When the module encounters data-template="demo:hello"
, it will try to find a function named
demo:hello
in all the modules known within the current XQuery context. If a function is found and its signature
follows a certain convention, it will be called and the <div>
will
either be replaced or enhanced by whatever the function returns.
The data-template
attribute must follow the function naming pattern.
It is also possible to pass static parameters to a template call.
Additional parameters go into one or more attributes
data-template-*
, where the * should be replace by the name of the parameter,
e.g. data-template-path="menu.html"
.
Templating Functions
A templating function is an ordinary XQuery function in a module which takes at least two parameters of a specific type. Additional parameters are allowed. If a function does not follow this convention it will be ignored by the framework.
For example, our "Hello world!" function could be defined as follows:
declare function demo:hello($node as node(), $model as map(*), $language as xs:string, $user as xs:string) as element(div) {
<div>
{
switch($language)
case "de" return
"Hallo " || $user
case "it" return
"Ciao " || $user
default return
"Hello " || $user
}
</div>
};
The two required parameters are $node
and $model
:
-
$node
contains the HTML node currently being processed: in this case, the<div>
element. -
$model
is an XQuery map with application data. It will be empty for now, but we'll see later why it is important.
Parameter Injection and Type Conversion
The additional parameters in the example above, $language
and
$user
, will be injected automatically. The templating framework tries to
make a best guess how to fill those parameters with values. It checks the following 3
contexts for parameters with the same name (in the order below):
-
if the current HTTP request contains a (non-empty) parameter with the same name as the parameter variable, this is used to set the value of the variable
-
if the HTTP session contains an attribute with the same name as the parameter variable, this is used to set the value of the variable
-
if the static parameters passed to the template call contain a parameter matching the variable name, it will be used
If neither 1 nor 2 lead to a non-empty value, the function signature will be checked
for an annotation %templates:default("name", "value1", ..., "valueN")
. See
below.
If "language" is passed as a parameter in the HTTP request, it will overwrite the static parameter we provided because the HTTP request is checked first.
The templating framework will also attempt automatic type conversion for all
parameters. If the parameter has a declared type of xs:integer
, it will try
to cast a parameter it finds into an integer. If the type is node()
, the
parameter value will be parsed into XML. These conversions may fail and this results in
an error passing a parameter with the wrong type.
Additional Annotations
Our "Hello world" example above does not preserve the div from which it was called,
but replaces it with a new one which lacks the "grey-box" class. This is the default
behavior. To preserve the enclosing div, we should add the XQuery annotation
%templates:wrap
to the function signature.
Another annotation can be used to provide a default value for a parameter:
%templates:default("parameter", "value1", "value2", ...)
. The first
parameter of the annotation must match the name of the parameter variable. All other
parameters in the annotation are used as values for the variable.
For example, set $language
to en
if the value cannot be
determined otherwise:
declare
%templates:wrap %templates:default("language", "en")
function demo:hello($node as node(), $model as map(*), $language as xs:string, $user as xs:string) as xs:string {
switch($language)
case "de" return
"Hallo " || $user
case "it" return
"Ciao " || $user
default return
"Hello " || $user
};
Because of the %templates:wrap
we can now remove the wrapping
<div>
in the function and just return a string now.
Using the Model to Keep Application Data
In a more complex application, a view will have many templating functions which all access the same data. For example, take a typical search page: there might be one HTML element to display the number of hits, one to show the query, and another one for printing out the results. All those components need to access the search result. How to do this in a templating framework?
This is where the $model
parameter comes into play. It is passed to all
template functions and they can add data to it. This is available to nested template
calls.
For example a search page:
<div class="demo:search">
<p>
Found
<span class="demo:hit-count"/>
hits
</p>
<ul class="demo:result-list"/>
</div>
The demo:hit-count
and demo:result-list
occur inside the
<div>
calling demo:search
. They are nested
template calls. demo:search
will perform the actual search operation, based
on the parameters passed by the user. Instead of directly printing the search result in
HTML, it delegates this to the nested templates.
demo:search
can be implemented as:
declare
%templates:wrap
function demo:search($node as node(), $model as map(*), $query as xs:string) as map(*) {
let $result :=
for $hit in collection($config:app-root)//SCENE[ft:query(., $query)]
order by ft:score($hit) descending
return $hit
return
map { "result": $result }
};
demo:search
differs from the functions we have seen so far in that it
returns an XQuery map and not HTML or some atomic type. If a templating function returns
a map, the templating framework will proceed as follows:
-
Add the returned map to the current
$model
map (adding it to the map keeps entries produced by any ancestor templates) -
resume processing the children of the current HTML node
The demo:hit-count
can now access the query results in
$model
:
declare function demo:hit-count($node as node(), $model as map(*)) as xs:integer {
count($model("result"))
};
Manual Processing Control
Inside a templating function, you can also call templates:process($nodes as
node()*, $model as map(*))
to have the templating module process the given
node sequence.
Warning:
You need to make sure you are not running into an endless loop by calling
templates:process
on the currently processed node.
A common pattern is to trigger templates:process
on the children of
the current node:
templates:process($node/node(), $model)
This is comparable to calling <xsl:apply-templates>
in XSLT and will have
the same effect as returning a map (see the section above), but with your templating
function having full control.
For example, it is sometimes necessary to first process all the descendant nodes
of the current element, then apply some action to the processed tree. The
documentation app has a function, config:expand-links
, which scans the
final document tree for links and expands them. The function is implemented as
follows:
declare %templates:wrap function config:expand-links($node as node(), $model as map(*), $base as xs:string?) {
for $node in templates:process($node/node(), $model)
return
config:expand-links($node, $base)
};
Set-Up
The templating module is entirely implemented in XQuery. It provides a single public
function, templates:apply
.
Important Note
The old version of the templating module used to be part of the deprecated shared-resources package, while a new and improved version is now available in its own expath package. We recommend to update to the new package with the short name templating, available on the eXist-db dashboard. To update, follow the 3 steps below:
-
change any dependency on shared-resources in your expath-pkg.xml to point to this package:
<dependency package="http://exist-db.org/html-templating"/>
-
update the module URI for any imports of the templating module:
import module namespace templates="http://exist-db.org/xquery/html-templating";
-
New standard templating functions will go into a separate module, so you may want to add the following import in addition to the one above, which will give you access to the lib:parse-params template function (and others in the future):
import module namespace lib="http://exist-db.org/xquery/html-templating/lib";
Calling the Templating from a Main XQuery
A complete main module which calls the templating framework to process an HTML file passed in the HTTP request body could look as follows:
(:~
: This is the main XQuery which will (by default) be called by controller.xq
: to process any URI ending with ".html". It receives the HTML from
: the controller and passes it to the templating framework.
:)
xquery version "3.0";
declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization";
import module namespace templates="http://exist-db.org/xquery/html-templating";
(:
: The following modules provide functions which will be called by the
: templating framework.
:)
import module namespace app="http://my.domain/myapp" at "app.xql";
declare option output:method "html";
declare option output:html-version "5";
(:
: We have to provide a lookup function to templates:apply to help it
: find functions in the imported application modules. The templates
: module cannot see the application modules, but the inline function
: below does see them.
:)
let $lookup := function($functionName as xs:string, $arity as xs:int) {
try {
function-lookup(xs:QName($functionName), $arity)
} catch * {
()
}
}
(:
: The HTML is passed in the request from the controller.
: Run it through the templating framework and return the result.
:)
let $content := request:get-data()
return
templates:apply($content, $lookup, ())
This module would be called from the URL rewriting controller. For example, we could
add a rule to controller.xq
to pass any .html resource to the above
main query (saved to modules/view.xq
):
(: Pass all requests to HTML files through view.xql, which handles HTML templating :)
if (ends-with($exist:resource, ".html")) then
<dispatch xmlns="http://exist.sourceforge.net/NS/exist">
<view>
<forward url="{$exist:controller}/modules/view.xql">
<set-attribute name="$exist:prefix" value="{$exist:prefix}"/>
<set-attribute name="$exist:controller" value="{$exist:controller}"/>
</forward>
</view>
<error-handler>
<forward url="{$exist:controller}/error-page.html" method="get"/>
<forward url="{$exist:controller}/modules/view.xql"/>
</error-handler>
</dispatch>
The only part of the main module code which might look a bit unusual is the inline lookup function: the templating module uses dynamic function calls to execute template functions in application modules. But unfortunately, XQuery modules can only "see" functions in their own context. There is therefore no way for the templating module to determine what functions are defined in application modules which are outside its context. So it needs to be "helped" by providing a callback function to resolve function references. The lookup function is defined in the main context and can access all the modules imported into the main module.
Normally you can just copy and paste the main module code as given above. To adapt it to your own application, just import your application modules and you're done.
Pre-defined Template Commands
The templating module defines a number of general-purpose templating functions which are described below. Functions marked as deprecated should not be used anymore. Those labelled with removed did exist in the old templating module (which was shipped in the shared-resources package) and has been removed from the newer templating package.
Functions marked as new have been added in the new templating package and are not available in the older module.
templates:include (deprecated)
Parameter |
Description |
---|---|
path |
path to the resources |
Includes the content of the resource given by parameter path
into the current
element. path
is always interpreted relative to the current application
directory or collection.
lib:include (new)
Parameter |
Description |
---|---|
path |
path to the resources |
Includes the content of the resource given by path
into the current
element. path
is always interpreted relative to the current application
directory or collection.
The children (if any) of the including element (i.e. the one triggering the lib:include function) are merged into the included content. To define where a child element should be inserted, you must use a @data-target attribute referencing an HTML id which must exist within the included fragment. If @data-target is missing, the element will be discarded.
This is a mechanism to inject content from the including element into the included content. For example, if the same menu or toolbar is included into every page of an application, but some pages should have additional options, you can use lib:include with lib:block to define the additional HTML to be inserted in a specific place.
templates:each
Parameter |
Description |
---|---|
from |
references a property in the model map which should be used as the source sequence to iterate |
to |
the name of the property to be added to the model map. This will contain the current iteration item. |
Retrieve the sequence identified by the map key from
in the
$model
map. If it exists, iterate over the items in its value (as a
sequence) and process any nested content once. During each iteration, the current
item is added to the $model
map using the key
to
.
templates:if-parameter-set
Parameter |
Description |
---|---|
name |
name of the parameter to check |
Conditionally includes content only if the given request parameter is set and not empty.
templates:if-parameter-unset
Parameter |
Description |
---|---|
param |
name of the parameter to check |
Conditionally includes content only if the given request parameter is not set or empty.
templates:surround (deprecated)
Parameter |
Description |
---|---|
with |
path to the resource which should be used as surrounding container |
at |
id of the HTML element which should be surrounded |
using |
(optional) id of an HTML element in the resource specified by with. If specified, only this element will be used as surrounding container - not the entire document. |
Surrounds its content with the contents of the XML resource specified in
with
. The at
parameter determines where the content is
inserted into the surrounding XML. It should match an existing HTML id
in the template.
The using
parameter is optional and specifies the id
of
an element in the with
resource. The current content will be surrounded
by this element. If this parameter is missing, the entire document given in
with
will be used.
The surround template instruction is used by all pages of the Demo application. The header, basic page
structure and menus are the same for all pages. Each page only contains a simple
<div>
with a template instruction:
templates:surround?with=templates/page.html&at=content
The instruction takes the content of the current element and injects it into the template page.
templates:form-control
templates:form-control
Use on <input>
and <select>
elements: checks the HTTP request
for a parameter matching the name of the form control and fills it into the value of
an input or selects the corresponding option of a select.
templates:load-source (removed)
templates:load-source
Commonly used with an <a>
element: opens the document referenced in the
href
attribute in eXide.
lib:parse-params (new)
Recursively expand template expressions appearing in attributes or text content, trying to expand them from request/session parameters or the current model.
Template expressions should have the form ${paramName:default text}
.
Specifying a default is optional. If there is no default and the parameter
cannot be expanded, the empty string is output.
To support navigating the map hierarchy of the model, paramName may be a sequence
of map keys separated by ?, i.e. ${address?street}
would first retrieve the map
property called "address" and then look up the property "street" on it.
The templating function should fail gracefully if a parameter or map lookup cannot be resolved, a lookup resolves to multiple items, a map or an array.