Zenon Page Templates (ZPT-JS): User's guide

last modified: 2016/15/11
author David Cana
contributed by David Cana

Zope Page Templates

ZPT-JS is a Javascript implementation of Zope Page Templates (ZPT). Because ZPT-JS isn't running in the context of Zope and isn't written with Python, there are necessarily some differences between ZPT-JS and ZPT. This document will concentrate on the ways that ZPT-JS differs from ZPT. For an introduction to ZPT refer to the chapter Using Zope Page Templates in the Zope Book. For a complete reference to ZPT, refer to the ZPT Reference.

First steps using ZPT-JS

How can we use ZPT-JS? The dictionary is used to define global variables.

An example of invoking ZPT-JS using CommonJS in a browser environment:

sample.js
"use strict";

var zpt = require( 'zpt' );

var dictionary = { 
    aString: "string",
    doggy: false,
    number1: 1,
    number100: 100,
    user: {
        name: "Bob", 
        age: function( ){
            return 25;
        }
    },
    items: [ 'item0', 'item1', 'item2' ]
};
    
zpt.run({
    root: document.body,
    dictionary: dictionary
});
    

Then browserify that file to build the final js file. And the html file is:

sample.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Some ZPT-JS examples</title>
        
        <script src="sample.js" defer></script>
    </head>
    <body>
        <h1>Some expressions</h1>
        <ol>
            <li data-tcontent="user/name">xxx</li>
            <li data-tcontent="string:help my ${user/name}">xxx</li>
            <li data-tcontent="doggy">not false</li>
            <li data-tcontent="not:doggy">not false</li>
            <li data-tcontent="eq: number1 number100">not true</li>
            <li data-tcontent="user/name | string:no friends">any friends?</li>
            <li data-tcontent="user2/name | string:no friends">any friends?</li>
            <li data-tcontent="items[0]">an item</li>
            <li data-tcontent="user/age()">user/age()</li>
        </ol>
    </body>
</html>
    

The invokation of ZPT-JS is at:

zpt.run({
    root: document.body,
    dictionary: dictionary
});
    

The first argument sets the DOM node where ZPT-JS will search its custom HTML tags: it can be a string or an array of them. The second one is the dictionary, a Javascript object that will be available to all custom HTML tags of ZPT-JS. You can also run ZPT-JS as a Jquery plugin:

"use strict";

var $ = require( 'jquery' );
require( '../../../js/app/jqueryPlugin.js' );

var dictionary = { ... }; // Your dictionary entries

$( 'body' ).zpt({
    dictionary: dictionary
});
    

If we don't use external macros ZPT-JS executes synchronously: no external file needs to be loaded. But if we use at least one external macro ZPT-JS needs to load one or more external files using HTTP. This makes ZPT-JS code executes asynchronously. Keep in mind this! An alternative method to deal with this is to call ZPT-JS like this:

var zptParser = zpt.buildParser({
    root: document.body,
    dictionary: dictionary,
    declaredRemotePageUrls: [ 'externalMacros-definitions2.html', 'externalMacros-definitions3.html' ]
});

zptParser.init(
    function(){
        zptParser.run();
        [ your code here ]
    }
);
    

That's OK. But... how can we use ZPT-JS at the server side (using node.js)? node-jsdom is needed when no browser is available:

"use strict";

var jsdom = require( 'node-jsdom' ).jsdom;

jsdom.env(
    '<!doctype html>'
	+ '<html>'
	+ '<body><h1 id="t1" data-tcontent="string:hello">a text</h1></body>'
	+ '</html>', 
    [ 'http://code.jquery.com/jquery.min.js' ], 
    function( err, window ) {
        
        // Check if an error occurs
        if ( err ) {
            console.error( err );
            return 1;
        }

        // Copy window to global
        global.window = window;
        
        // Copy from window to local vars
        var $ = window.$;
        var document = window.document;

        // Parse template
        var zpt = require( 'zpt' );
        
        zpt.run({
            root: document.body,
            dictionary: {}
        });
        
        console.log( 'Done!' );
    }
);
    

Take a look at jsdom docs for more info about jsdom.

ZPT-JS tags

The name of tags are not the same because if they were the resulting HTML douments will not be well formed. The list of tags with their equivalences are:

ZPT tag name ZPT-JS tag name Description
metal:define-macro data-mdefine-macro Defines a macro
metal:define-slot data-mdefine-slot Defines a slot to make possible to fill it later
metal:fill-slot data-mfill-slot Fills a slot
metal:use-macro data-muse-macro Invokes a macro
tal:attributes data-tattributes Replace the value of one or more attributes
tal:condition data-tcondition Display or hide nodes depending on a condition
tal:content data-tcontent Replace the content of the element
tal:define data-tdefine Define one or more variables
tal:on-error data-ton-error Handle errors
tal:omit-tag data-tomit-tag Remove an element, leaving the content of the element
tal:repeat data-trepeat Repeat an element
tal:replace data-treplace Replace the content of an element and remove the element leaving the content

The tags can be changed. Customize the defaultTag variable in js/app/context.js file. You can also use context.setTags( tags ) to define the tags programmatically.
Important: after this point we will refer to tags as the ZPT-JS tag name.

Using ZPT's original tags

The data-* attributes is used to store custom data private to the page or application. These are the standard in HTML5. If you prefer to use the original ZPT's attributes use context.useOriginalTags() method:

original-tags-sample.js
"use strict";

var zpt = require( 'zpt' );

var dictionary = { 
    aString: "string",
    doggy: false,
    number1: 1,
    number100: 100,
    user: {
        name: "Bob", 
        age: function( ){
            return 25;
        }
    },
    items: [ 'item0', 'item1', 'item2' ]
};

// Don't forget to declare to use original tags!
zpt.context.useOriginalTags();

zpt.run({
    root: document.body,
    dictionary: dictionary
});
    
original-tags-sample.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Some ZPT-JS examples</title>

        <script src="original-tags-sample.js" defer></script>
    </head>
    <body>
        <h1>Some expressions</h1>
        <ol>
            <li tal:content="user/name">xxx</li>
        </ol>
    </body>
</html>
    

Evaluation order

The order evaluation of attributes in ZPT-JS is not equal than ZPT's. The new order is:

  1. data-trepeat
  2. data-ton-error
  3. data-mdefine-macro
  4. data-tdefine
  5. data-tcondition
  6. data-tomit-tag
  7. data-treplace
  8. data-tattributes
  9. data-tcontent
  10. content
  11. data-muse-macro

Path Expressions

The first element in a path expression must be a variable, a method call, a function call or a literal.

Literals

Integer, float and boolean literals are defined in the same way as in the Javascript language. String literals are delimited by single quotes. Some example literals:

Variables

A variable is either predefined, defined via a data-tdefine attribute, or passed in to the template at runtime. The following variables are predefined:

The here variable (the context) is not defined in ZPT-JS; it is implicit. The scope of all variables in dictionary is global. Take a look to Invoking ZPT-JS to understand how you can define variables of the dictionary.
The scope of the variables defined via a data-tdefine attribute is local. An example:

<div data-tdefine="a number">
    <span data-treplace="a">5</span>
</div>
<span data-treplace="a">null</span>
    

At the first replace the value in number will be used. At the second replace, a null value (the a variable is out of scope).
Global variables are also implemented. Their scope is all document after their definition:

<div data-tdefine="global a number">
    <span data-treplace="a">5</span>
</div>
<span data-treplace="a">5</span>
    

The following variables are defined in ZPT but not in ZPT-JS: here, template, resolver, options, CONTEXTS, root, container, request, user, modules. The following variables are defined in ZPT but aren't yet implemented in ZPT-JS: nothing, default, attrs. If you need these, holler.

Path traversal

Following the initial path element, path elements are either properties or methods of the preceding object. Examples:

The last element in a path expression may resolve to null, but if an intermediate element resolves to null an Exception will be thrown.

Arrays and objects

Array members may be accessed using the same syntax as in Javascript. Any number of dimensions are supported. The expression inside the array accessor may be any TALES expression and must evaluate to an integer value.

Object properties may be accessed using the same syntax as in Javascript too.

Javascript expressions

Javascript expressions work just like Python expressions in ZPT except that the Javascript language is used instead of Python. Any legal Javascript expression may be evaluated. Some examples:

Exists expressions

It evaluates as a boolean expression. If the expression is any of the next:

the expression evaluates to false. Otherwise the expression evaluates to true. If an exception is thrown trying to evaluate the expression, it evaluates to false.

Not expressions

Not expressions work more or less like in ZPT. The expression to which not: is applied must first be cast to a boolean. The result is then negated.

Math expressions

You can do some math operations using this expressions:

In all cases, x and y are assumed integers. You can use more than 2 values to operate. You can use parenthesis. Some examples:

Boolean expressions

The list of available boolean expressions are:

All operators uses lazy evaluation. OR and AND expressions support 2 or more operators. COND expression only support 3. An example:

Comparison expressions

The 4 available comparison expressions are:

Some examples:

Jquery expressions

ZPT-JS supports some jquery expressions:

<div>
    <div>value1 = <span id="value1" class="values">10</span></div>
    <div>value2 = <span id="value2" class="values">20</span></div>
    <div>value3 = <span id="value3" class="values">30</span></div>
</div>

<div>
    <div data-tcontent="$('#value1')">must be 10</div>
    <div data-tcontent="$('.values')">must be 10,20,30</div>
    <div data-tcontent="+: $('.values')">must be 10 + 20 + 30 = 60</div>
    <div data-tcontent="+: 100 $( '.values' )">must be 100 + 10 + 20 + 30 = 160</div>
</div>
    

Other expressions

String expressions behave exactly as in ZPT. Python expressions are not supported in ZPT-JS.

The window object

ZPT-JS register the window object automatically, so global variables defined via javascript can be used easily:

<div data-tcontent="window/globalVar">a string</div>
    

TAL Statements

All TAL statements behave almost exactly as in ZPT, except:

Repeat expressions

There are a few minor variations for tal:repeat. The repeat expression must evaluate to an array.

Lists expressions

A list is defined as an enumeration of items in brackets. Some examples:

You can iterate through lists using loops:

Another ways of using lists is as range expressions:

Lists are very versatile:

...and can be used in arithmethic operations:

Don't forget to not use spaces inside ranges!

Attributes

ZPT's version of data-tattributes forces to uses key/values pairs. ZPT-JS also allows to use javascript objects. If we define a dictionary this way:

var dictionary = { 
    textareaAttrs: {
        rows: 10,
        cols: 100
    }
};
    

...we can use this in a template:

<textarea data-tattributes="placeholder 'Write something here!';
                            textareaAttrs;
                            maxlength 200"></textarea>
    

METAL

METAL statements behave exactly as in ZPT. The main difference, which is really a difference in Path expressions, is the means of finding another template which contains macros. There is no Zope tree in which to locate templates. Use-macro tag uses expressions (ZPT's version does not, it uses literals).

Local macros

Local macros are defined at the same HTML file where they are invoked. An example of definition of a local macro:

<ul data-mdefine-macro="list">
    <li data-trepeat="item items">
        <span data-tcontent="item">An item</span>
    </li>
</ul>
    

That macro generates an unordered list iterating through the items variable. Let's invoke them; to do this, the next HTML code must be at the same file.

<div data-tdefine="items [10 20 30]" data-muse-macro="'list'">
    Macro goes here
</div>
    

ZPT-JS allows to uses expressions when using macros. The next HTML code invokes the same macro if the value of listMacro variable is list:

<div data-tdefine="items [10 20 30]" data-muse-macro="'listMacro'">
    Macro goes here
</div>
    

External macros

External macros are defined at a different page that where they are invoked. They only differ how they are invoked; if we want to invoke the macro list defined in macros.html file:

<div data-tdefine="items [10 20 30]" data-muse-macro="'list@macros.html'">
    Macro goes here
</div>
    

External macro files must be preloaded before the template is rendered, so code is async:

var zptParser = zpt.buildParser({
    root: document.body,
    dictionary: dictionary
});

zptParser.init(
    function(){
        zptParser.run();
        [ your code here ]
    }
);
    

Because external macro files must be preloaded before the template is rendered, ZPT-JS must to know the list of external files invoked in the template. If we use literal string expressions or expressions that can be evaluated using only dictionary there is nothing to do. But if we use an expression that can not be resolved at first like this:

<div data-muse-macro="anObject/templateName">
    Macro goes here
</div>
    

If anObject/templateName evaluates to aMacro@macros.html and there is no rederence to macros.html in other macro invokations, that macro invokation will throw an exception: Macros in URL 'macros.html' not preloaded!. To resolve this issue we must set manually the list of external macro files we want to use when executing the ZPT call:

var zptParser = zpt.buildParser({
    root: document.body,
    dictionary: dictionary,
    declaredRemotePageUrls: [ 'macros.html', 'moreMacros.html' ]
});

zptParser.init(
    function(){
        zptParser.run();
        [ your code here ]
    }
);
    

URLs are by default relative to current URL. You can also use absolute URLs:

var zptParser = zpt.buildParser({
    root: document.body,
    dictionary: dictionary,
    declaredRemotePageUrls: [ '/path/to/your/macro/macros.html' ]
});
...
    

And in your HTML code:

<div data-muse-macro="'myMacro@/path/to/your/macro/macros.html'">
    Macro goes here
</div>
    

Context object provides a method to set a prefix to all relative URLs:

context.setExternalMacroPrefixURL( '/path/to/your/templates/' );
    

Then if you use an URL like macros.html it will be replaced by /path/to/your/templates/macros.html.

Formatters

A formatter allows to format the evaluation of an expression. ZPT-JS includes some formatters:

You can register other formatters of your own:

context.registerFormatter( 
    'myCustomFormatter', 
    function( value ){
        return "$" + value;
    }
);
    

After registering it you will be able to use it as usual:

It is possible to run custom formatters without registering them. They must be functions defined in the dictionary:

I18n and l10n

Basics about i18n

Nowadays ZPT-JS has some i18n capabilities. How do they work? Let's see an example:

i18n.js
"use strict";

var zpt = require( '../../../js/app/main.js' );
var I18n = require( '../../../js/app/i18n/i18n.js' );
var I18nBundle = require( '../../../js/app/i18n/i18nBundle.js' );

/* I18n maps init */
var msg = {
    en : {},
    es : {}
};

/* English i18n messages */
msg.en[ '/CONF/' ] = {
    language: 'en',
    locale: 'en-US'
};
msg.en[ 'Hello world!' ] = 'Hello world!';
msg.en[ 'Results msg' ] = '{GENDER, select, male{He} female{She} other{They} }' +
    ' found ' +
    '{RES, plural, =0{no results} one{1 result} other{# results} }';

/* Spanish i18n messages */
msg.es[ '/CONF/' ] = {
    language: 'es',
    locale: 'es-ES'
};
msg.es[ 'Hello world!' ] = '¡Hola mundo!';
msg.es[ 'Results msg' ] = '{ GENDER, select, male{Él} female{Ella} other{Ellos} }' +
    ' ' +
    '{ RES, plural, =0{no } other{} }' +
    '{ GENDER, select, male{ha} female{ha} other{han} }' +
    ' encontrado ' +
    '{ RES, plural, =0{ningún resultado} one{un único resultado} other{# resultados} }';

// Create I18n and i18nBundle instances
var i18nES = new I18n( 'es', msg[ 'es' ] );
var i18nEN = new I18n( 'en', msg[ 'en' ] );
var i18nBundle = new I18nBundle( i18nES, i18nEN );

// Init dictionary
var dictionary = {
    'i18nBundle': i18nBundle
};

// Parse template
zpt.run({
    root: document.body,
    dictionary: dictionary
});
    
i18n.html
<!DOCTYPE html>
<html lang="es">
    <head>
        <meta charset="utf-8">
        <title>Some I18n examples</title>
        
        <script src="i18n.js"></script>
    </head>
    <body>
        <h1>Some I18n expressions</h1>
        <ol data-ilanguage="'en'" data-idomain="i18nBundle">
            <li>
               ¡Hola mundo! = 
               <span data-tcontent="tr: 'Hello world!'">Must be ¡Hola mundo!</span>
            </li>
            <li>
               Él ha encontrado 10 resultados = 
                <span data-tcontent="tr: 'Results msg' ( GENDER 'male'; RES 10 )">Must be 'Él ha encontrado 10 resultados'</span>
            </li>
        </ol>
    </body>
</html>
    

Some remarks about this:

Some examples

Some examples of i18n tags in action:

Some examples of valid data-idomain attributes:

Working with domains

In the previous example the domain was a simple I18nBundle instance. This forces to use big maps with all the messages of one language. This can be awful if the amount of messages is big. data-idomain tag supports also a list of I18nBundle instances, so the messages will be searched in the same order.

Therefore, data-idomain="i18nBundle1 i18nBundle2" allows to organise your i18n messages in 2 maps. The first one can contain general messages and the second one more particular messages (for example).

data-idomain also supports nested definitions:

<div data-idomain="i18nBundle1">
   <span data-tcontent="tr: 'Hello world!'">
        ¡Hola mundo!
   </span>
   <span data-idomain="i18nBundle2" data-tcontent="tr: 'Hello world!'">
        ¡¡¡Hola mundo 2!!!
   </span>
</div> 
    

The first data-tcontent will search only in i18nBundle1. The second one will search first in i18nBundle2 and if it is not found will search in i18nBundle1.

Loading messages from JSON files

ZPT-JS makes it easy loading i18n messages from JSON files:

"use strict";

var zpt = require( '../../../js/app/main.js' );

var dictionary = {
    ...
};

var zptParser = zpt.buildParser({
    root: document.body,
    dictionary: dictionary,
    i18n: {
        urlPrefix: './i18n/',
        files: {
            'es': [ 'es1.json', 'es2.json' ],
            'en': [ 'en1.json', 'en2.json' ]
        }
    }
});

zptParser.init(
    function(){
    
        // Add I18nBundle instances to dictionary
        var dictionaryExtension = {
            i18nBundle1: new I18nBundle( dictionary.i18nES1, dictionary.i18nEN1 ),
            i18nBundle2: new I18nBundle( dictionary.i18nES2, dictionary.i18nEN2 )
        };
        $.extend( true, dictionary, dictionaryExtension );
            
        zptParser.run();
    }
);
    

ZPT will add to dictionary these vars:

The order in arrays is inverted: first the last files, then the first.

The bundles (i18nBundle1 and i18nBundle2) are not required, add to the dictionary if you need them.

Numbers

ZPT-JS uses Intl as i18n API for numbers. Let's see some examples:

Take a look on NumberFormat options to see all available options.

Currencies

ZPT-JS uses Intl as i18n API for currencies. Let's see some examples:

Options are the same as numbers (NumberFormat options) plus some specific options of currencies.

Dates and times

ZPT-JS uses Intl as i18n API for date and times. Let's see some examples:

Take a look on DateTimeFormat options to see all available options.

Custom expressions

ZPT-JS makes it easy to register custom expression managers. For example, we want to develop an expression manager that operates similarly to the ++ C operator. First we must implement the class (incExpression.js file):

"use strict";

var context = require( '../../../js/app/context.js' );
var evaluateHelper = require( '../../../js/app/expressions/evaluateHelper.js' );
var $ = require( 'jquery' );

var IncExpression = function( varNameToApply ) {
    
    var varName = varNameToApply;
    
    var evaluate = function( scope ){
        
        var value = scope.get( varName );
        
        if ( ! evaluateHelper.isNumber( value ) ) {
            throw 'Error trying to inc number, not a number!';
        }
        
        scope.set( varName, ++value, true );
        
        return value;
    };
    
    return {
        evaluate: evaluate
    };
};

IncExpression.removePrefix = true;
IncExpression.getPrefix = function() {
    return '++' + context.getConf().expressionSuffix;
};
IncExpression.getId = IncExpression.getPrefix;

IncExpression.build = function( string ) {
    return new IncExpression( string.trim() );
}

module.exports = IncExpression;
    

Don't forget to register that class!

var expressionBuilder = require( '../../../js/app/expressions/expressionBuilder.js' );
var IncExpression = require( './incExpression.js' );

expressionBuilder.register( require( IncExpression ) );
    

Building and testing

At first use the next command to initialize a beefy server (port 9966) to make it easy to test ZPT-JS without browserifying by hand and a HTTP server (port 9000) to serve static files (HTML, CSS and JSON files).

$ npm run start
    

ZPT-JS uses QUnit as testing framework. For testing ZPT-JS open test/index.html or http://localhost:9000/test/ with your favourite browser. All tests passed using last versions of Mozilla Firefox (55.0.2, 64-bit). Perhaps some tests fail using other browsers!

ZPT-JS uses Grunt as task runner tool. If you want to modify and rebuild ZPT-JS the next command lines are useful:

$ grunt browserify        // Build all test js files; also build a standalone version
$ grunt browserify:[file] // Build a single js file; use 'standalone' to build a standalone version
$ grunt compress          // Compress files and folders of this project into dist/zpt_yyyy-mm-dd_hhmm.tar.gz
    

More examples

Please, take a look to test files in test/ to view more ZPT-JS examples.