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

  1. 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

  2. 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

  3. 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:

  1. Add the returned map to the current $model map (adding it to the map keeps entries produced by any ancestor templates)

  2. 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:

  1. change any dependency on shared-resources in your expath-pkg.xml to point to this package:

    <dependency package="http://exist-db.org/html-templating"/>
  2. update the module URI for any imports of the templating module:

    import module namespace templates="http://exist-db.org/xquery/html-templating";
  3. 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.