HTML Templating Module

(1Q18)


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 class (or data-template, see below) attributes following a simple convention and tries to translate them into XQuery function calls. By using class 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.

In the simplest case, a template call inside a class attribute is just the name of a function known in the XQuery context. To start with the usual "Hello world" example:

<div class="demo:hello grey-box">Greet the user</div>

When the module encounters demo:hello, it will try to find a function named "demo:hello" in all the modules known to the XQuery. If this function's signature follows a certain convention (see below), it will be called and the <div> will either be replaced or enhanced by whatever the function returns.

Please note that the additional class "grey-box" does not interfere with the template call and is ignored by the templating framework. The templating framework will only take a closer look at class names which follow the prefix:local-name pattern. grey-box is probably be a class used for CSS styling, so we don't want to remove it.

It is possible to pass static parameters to a template call. These must be encoded as URI parameters, for instance: demo:hello?language=de. A static parameter will be passed to the XQuery function as a fallback value, if it cannot be determined by looking at the HTTP context (see below).

HTML5 Method

Instead of "abusing" class attributes to encode template calls or other application-specific information, HTML5 provides a standard method for adding data to an element, using data-* attributes. This approach is supported in the templating framework (beginning with version 0.3.0 of the shared-resources package).

The data-* attributes must follow the function naming pattern. The templating function to call has to be specified in an data-template, optional parameters go into one or more attributes data-template-*.

For example the previous example could be rewritten as:

<div data-template="demo:hello" data-template-language="de"></div>

The templating framework supports both alternatives.

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. 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.xql
 : 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";

import module namespace templates="http://exist-db.org/xquery/templates" at "templates.xql";

(: 
 : 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 exist:serialize "method=html5 media-type=text/html";

(:
 : 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.xql to pass any .html resource to the above main query (saved to modules/view.xql):

(: 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 need to "help" it 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 adopt it to your own application, just import your application modules and you're done.

Integration with eXide

The "New Application" templates in eXide already include the HTML templating module and configures the URL rewriting to call this for any path ending in .html. Using eXide is therefore the easiest way to get started with the templating framework.

Please consult the Getting Started with Web Application Development guide.

Where to Find the Module?

If you generate your application with eXide, a copy of the HTML templating module will be included, so you can customize it. If you rather want to make sure you have the latest version of the templating module: the shared-resources application also exports the module. This will always be the latest version. You could therefore define a dependency on the shared-resources app (see the packaging documentation). You can now just import the module by its namespace URI, but without specifying a location:

import module namespace templates="http://exist-db.org/xquery/templates";

The documentation and demo applications all read the templating module from shared-resources.

Pre-defined Template Commands

The templating module defines a number of general-purpose templating functions which are described below.

templates:surround is probably the most powerful one and used by almost all HTML views.

templates:include

templates:include?path=path-to-xml-resource

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.

templates:each

templates:each?from=map-key&amp;to=map-key

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

templates:if-parameter-set?param=request-parameter

Conditionally includes content only if the given request parameter is set and not empty.

templates:if-parameter-unset

templates:if-parameter-unset?param=request-parameter

Conditionally includes content only if the given request parameter is not set or empty.

templates:surround

templates:surround?with=xml-resource&at=id&using=id

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

templates:load-source

Commonly used with an <a> element: opens the document referenced in the href attribute in eXide.