templz

0.2.0 • Public • Published

Templz

Logic-less <template>s with a sense.

A Mustache-inspired template engine that can work with string templates and DOM elements. Even together!

Installation

As a standalone Javascript file:

<script type="text/javascript" src="templz.js"></script>

When using an AMD loader (like RequireJS in this example), you should require the module at the beginning like this:

require([ "templz" ], function(Templz) {
    ...
});

The package is also available with Bower:

$ bower install templz

In node.js/io.js:

$ npm install templz
var Templz = require("templz");

The module returns the Templz object, together with its static methods and properties.

Although Templz can be used just for string templates, it can be used for DOM templates in a node.js/io.js environment too when some DOM implementation is provided. This is an example using jsdom:

var Templz = require("templz"),
    jsdom = require("jsdom");
 
jsdom.env("<html></html>", function(errors, window) {
    if (window) {
        var newTemplz = new Templz.Context(window);
 
        // Do stuff with newTemplz just like it's Templz
    }
});

More info about the Templz.Context class later (hint: the Templz object itself is an istance of Templz.Context).

Usage

The basic concept is to create a compiled template out of a string, a DOM element or a DocumentFragment, and then use it to render the final result using the data and the partial passed to the render function. When used with plain strings, it behaves just like an implementation of Mustache's specs:

var template = Templz.compile("This is {{city}}!");
 
console.log(template.render({ city: "Atheeeens" });
// "This is Atheeeens!"

The resulting template object a string template (template.type === Templz.STRING_TEMPLATE).

When an element is passed to the Templz.compile function, it also uses the directives represented as specially named attributes to define Mustache-like constructs:

<template id="myTemplate">
    This is one of the following {{kind}}:
    <ul>
        <li tpz-section="items">{{type}} (<span tpz-name="quantity"></span>);</li>
    </ul>
    {{#warning}}
    You <em class="{{chanceClass}}">may</em> expect some issues.
    {{/warning}}
</template>
var template = Templz.compile(document.getElementById("myTemplate"));
 
document.body.appendChild(template.render({
    kind: "fruits",
    items: [
        { type: "apples", quantity: 20},
        { type: "oranges", quantity: 10},
        { type: "pears", quantity: 15}
    ],
    warning: { chanceClass: "plain" }
}));

As you can see, you can mix Mustache tags with Templz' own directives, and use Mustache templates in attributes too. This is the result, given as a DocumentFragment:

This is one of the following fruits:
<ul>
    <li>apples (<span>20</span>);</li>
    <li>oranges (<span>10</span>);</li>
    <li>pears (<span>15</span>);</li>
</ul>
You <em class="plain">may</em> expect some issues.

In this case, template.type === Templz.FRAGMENT_TEMPLATE.

Although Templz has been developed with the <template> element in mind, it can work with any Element or DocumentFragment nodes, so you can use <template> to define your DOM templates in browsers that doesn't support it (of course, be sure to make them invisible using display: none or other method, and keep in mind that they won't be inert, i.e. elements like <img>, <script> and <link> may perform HTTP requests and run scripts).

Note: when mixing Mustache tags with DOM elements, keep in mind that you can create sections and inverted sections encompassing DOM elements, but only if the opening and closing tags of the sections belong to text nodes with the same parent node. This, for example, is not a valid template:

<div>
    Some text.
    <br>
    {{#section}}
    Section start.
</div>
<div>
    Section end.
    {{/section}}
    <br>
    Final text.
</div>

Directives

As you can see from the preliminary examples, Templz' directives are defined as attributes with names beginning with tpz-. This prefix is defined in the Templz.prefix property, so you can change it to your needs (for example, setting it to data-tpl to make it HTML5 compliant).

All the directive attributes will be removed in the final result.

  • tpz-name

    This is the equivalent of the usual value interpolation of Mustache. The node is filled with the respective value from the data passed to the render function, with troublesome HTML entities escaped, and the existing content is discarded.

    <span tpz-name="value">This text will be removed in the rendered result.</span>
  • tpz-content

    This behaves like tpz-name, but the interpolated value isn't escaped. This is the equivalent of {{{...}}} or {{&...}} tags in Mustache.

    The content in the following element will <em>not</em> be escaped:
    <div tpz-content="html"></div>
  • tpz-section

    Templz' sections are sort of like Mustache's sections. The element is kept or discarded in the rendered result if the respective value in the data is truthy or falsy (according to the Javascript definitions). But if the value is an array, the element is repeated for each item of the array, and rendered using the data contaned in the item.

    <div class="user-box" tpz-section="users">
        {{username}} <span tpz-name="role"></span>
    </div>
  • tpz-empty

    Templz also supports inverted sections, i.e. elements that are rendered if and only if the respective data is falsy, similarly to {{^...}} tags in Mustache.

    Operation complete.
    <div tpz-empty="items" clas="warning-message">
        Warning: no item has been found.
    </div>
  • tpz-partial

    Elements defined as partial wrappers are filled with the rendered result of the respective partial template passed to the render method. If no corresponding partial is found, the node is discarded from the final result.

    Upload your photo here:
    <div data-upload-type="photo" tpz-partial="upload"></div>
     
    Upload your document here:
    <div data-upload-type="document" tpz-partial="upload"></div>
  • tpz-prefix

    This changes the directive prefix for the subsequent compilation of the template. Keep in mind that different user agents handle attributes in different ways, so using lowercase prefixes is recommended.

    This uses the default prefix: <span tpz-name="value"></span>
    <div tpz-prefix="hello-">
        A new way: <span hello-name="world"></span>.
        This tag will not be filled: <span tpz-name="world">exactly</span>,
        and will keel the <code>tpz-name</code> attribute.
    </div>
    Restoring the default prefix: <span hello-prefix="tpz-" tpz-name="confirm"></span>.

    Unlike the previous directives, which are mutually exclusive, tpz-prefix can be used together with other directives.

  • tpz-tags

    This changes the delimiters of text tags, similarly to {{=...}} tags in Mustache. Just as in Mustache, the opening and closing tags should be separated by whitespaces.

    <div tpz-tags="(% %)" class="(% theClass %)">
        A list: (%#list%) (%.%), (%/list%) and nothing else matters.
    </div>

    This one can be used with other directives, like tpz-prefix. The default tag delimiters are set in Templz.tags as an array of two strings.

Partials

Partials can be defined in the key/value pairs argument passed after the data object in the render method. Partials can be compiled templates, either of string or fragment type, or strings, or DOM nodes. Strings and nodes are first compiled using the default tags and prefix.

If the rendering template is a fragment template, then fragment partials are inserted seamlessly as collections of nodes. If the partial is a string template, it's inserted into the result as a text node.

On the other hand, if the template that's being rendered is a string template, a string partial is interpolated as specified by Mustache. But if it's a fragment template partial, the result is inserted after a serialization of the resulting DocumentFragment node.

Data binding

Compiled templated can be bound do a data objects, allowing them to automatically update the rendered result when something in the data object changes. This creates a BoundTemplate object. Of course, this makes most sense only wen the result is attached to the document.

Suppose we have a template like:

<template id="personList">
    The current list is:
    <ul>
        <li tpz-section="list">{{.}}</li>
    </ul>
</template>

Now we can bind this template to some data, and add it to the document:

var template = Templz.compile(document.getElementById("personList")),
    data = { list: [ "Mark", "Bob" ] },
    boundTemplate;
 
boundTemplate = template.bindToData(data);
boundTemplate.appendTo(document.body);

This is pretty much equivalent to appending the result of template.render(data) to the body, but now if we change something in data the result is automatically rendered in the document. For example, if we do data.list.push("Wade"), we'd get without further intervention:

<html>
    ...
    <body>
        ...
        The current list is:
        <ul>
            <li>Mark</li>
            <li>Bob</li>
            <li>Wade</li>
        </ul>
    </body>
</html>

Alternatively, data binding can be performed on elements that are already in the document:

<div id="currentRecipient">
    Name: <b tpz-name="name"></b><br/>
    Email: <a href="mailto:{{email}}" tpz-name="email"></a><br/>
    Role: <i tpz-name="role"></i>
</div>
var data = { name: "John Doe", email: "john@doe.com", role: "private" };
Templz.bindElementToData(document.getElementById("currentRecipient"), data);

Support notes on data-binding

Data bound templates rely on EcmaScript 7's Object.observe , which is natively supported by Blink-based environments (Chrome, Opera, node.js 0.11.13+, io.js). If Object.observe isn't supported, the .bindToData and Templz.bindElementToData methods aren't defined, but this can be solved using of the shims that could fill the gap:

If you use RequireJS to load Templz, you can load the polyfill in advance by shimming a dependency:

if (!Object.observe)
    requirejs.config({
        shim: {
            templz: { deps: [ "object.observe" ] }
        }
    });

API

  • Templz.compile(template[, tags[, prefix]])

    This is the entry point of Templz. You pass a string or a node, and you get a compiled template back.

    The tags argument is to define the Mustache tag delimiters used for parsing the template, and it can be either a string consisting on the opening and closing delimiters separated by whitespace (e.g. "(% %)") or an array of two strings (e.g. [ "(%", "%)" ]). If omitted, Templz.tags is used.

    The prefix argument is to define the Templz directive prefix. If omitted, Templz.prefix is used.

  • Templz.bindElementToData(element, data)

    This creates

  • Templz.serialize(fragment)

    Serializes the children of a Node object into a string. Developed with DocumentFragment nodes in mind, it can work with Element nodes too.

    It relies on the implementation of innerHTML, so if that's broken in some way (like in IE8-), this has the same problems. The best results are obtained if the enviroment supports <template> elements. If you need some more robust and platform independent serializer, there are project that can do for you (like parse5, for node.js).

  • Templz.createFragment(html)

    Creates a DocumentFragment node, initialized with the given content passed as raw HTML. See Templz.serialize for notes about HTML handling.

  • Templz.bindElementToData(element, data, [tags], [prefix], [partials])

    Creates a template out of the given element, and binds it to the given data right in place. Returns a BoundTemplate object.

  • Templz.tags

    Opening and closing delimiters for Mustache tags. See Templz.compile for details. It's normally set to [ "{{", "}}" ].

  • Templz.prefix

    Templz own directive attribute prefix. It defines the prefix of the node attributes the Templz looks for when parsing fragment templates. Initially set to "tpz-".

  • Templz.STRING_TEMPLATE, Templz.FRAGMENT_TEMPLATE

    Constants to check the type property of compiled templates. Currently set to "string" and "fragment", respectively.

  • Templz.Context(window, [document])

    Class that defines a context for Templz to operate with. If document is not provided, window.document is used.

    Defining a context is fundamental for Templz to work with fragment templates. The Templz object itself is an instance of this class, initialized with the window object that is commonly provided in a browser environment. All the above methods and properties of Templz come from the prototype of Templz.Context.

    However, in a node.js/io.js environment (or in a web worker, for the matter), the window object isn't defined, so the Templz object can't work with fragment templates, and even the methods createFragment and serialize aren't defined. To fix this, one should provide an implementation of the DOM (like jsdom) and explicitly create a context for Templz.

  • ParsedTemplate.prototype.type

    The type of the compiled template. It may equal Templz.STRING_TEMPLATE or Templz.FRAGMENT_TEMPLATE.

  • ParsedTemplate.prototype.render([data, [partials]])

    Renders a compiled template, returning a string (if it's a string template) or a DocumentFragment node (if it's a fragment template).

    data is an object containing the data used to build the result, just as in Mustache.

    Similarly, partials is a key/value map that provides the partials for rendering. The given partials can be ParsedTemplate objects of either types, or strings or nodes. In these two last cases, they're compiled using Templz.compile.

  • ParsedTemplate.prototype.bindToData(data, [partials])

    Binds a template to a data object, getting a BoundTemplate object in return.

  • BoundTemplate.prototype.bind(data, [partials])

    Sets the data (and eventually partials) that which the template is bound to.

  • BoundTemplate.prototype.unbind()

    Unbinds the template, de facto ending any live behaviour.

  • BoundTemplate.prototype.appendTo(parent), BoundTemplate.prototype.insertBefore(element)

    Attaches the template to DOM tree.

  • BoundTemplate.prototype.getTemplate()

    Returns the underlying ParsedTemplate object. Useful when the BoundTemplate object has been create with Templz.bindElementToData.

Browser support

Templz has been tested in the following environments:

  • Google Chrome 35+ (probably working fine with previous versions too)
  • Mozilla Firefox 30+ (see above)
  • Internet Explorer 7+
  • Opera 12.15, 22+ (probably fine with 15-21 too)
  • node.js 0.10+

In general, as long as string templates are used, it should work in every Javascript environment that's not horribly outdated. When dealing with fragment templates, every environment with a standard DOM implementation should work fine. Special workarounds are used for older versions of Internet Explorer (it's always about old IE...), but they don't cover things like element serialization or text node normalization, so expect some quirky behaviours.

HTMLTemplateElement support is granted in Chrome 26+, Firefox 22+, Opera 15+ and Android 4.4+. For more details, check CanIUse.

Known issues

Mustache conformity for string templates isn't perfect. But compared to the two main alternatives, Twitter's Hogan and janl's Mustache implementation, Templz behaves better, failing only 7 tests out of 122 (compared to 8 for Hogan and 16 for Mustache).

The main source of problems is the conformity with lambda functions. Namely, Templz doesn't parse the result of the lambda call, and doesn't call the function for sections (it just considers it as a truthy value). Something similar happens for Hogan and janl's Mustache, too.

Moreover, Templz indents all the lines of an indented partial. This is correct, except when the new lines come from an unescaped interpolation (triple mustache or ampersand tag).

To-do

  • Engine rewrite for overall optimization
  • Directives to dynamically set node attributes
  • Complete test cases and Mustache specs conformity
  • Comparison benchmarks
  • More tests...

Under consideration

  • Filters
  • Extensions
  • Two-way data binding
  • Templz.render for direct template rendering
  • Template caching: compiling templates allows developers to handle their own caches
  • Lambda functions as sections
  • Complete XML support
  • Rendering comment and CDATA nodes

License

The MIT License (MIT)

Copyright (c) 2014-2015 Massimo Artizzu (MaxArt2501)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Package Sidebar

Install

npm i templz

Weekly Downloads

0

Version

0.2.0

License

MIT

Last publish

Collaborators

  • maxart