@lawrencesim/templatize
v2.4.0
Published
Basic templating code, similar to Mustache.js. It originally started as needing a very simplistic template library, hence creating my own version, before snowballing requirements basically made it almost the same functional capacity as Mustache.js. On the
Downloads
6
Readme
Templatize
Basic templating code. It originally started as needing a very simplistic template library, hence creating my own version, before snowballing requirements (and also just personal curiosity on where I could take it) turned it into a powerful templating library of its own.
Lawrence Sim © 2024
Contents
Installation
Templatize may be installed via NPM: npm install @lawrencesim/templatize
.
Once installed, the module can be imported or required as necessitated by your project.
// ES6 module import
import Templatize from '@lawrencesim/templatize';
// CommonJS require
const Templatize = require('@lawrencesim/templatize').Templatize;
Otherwise, the files in the folder dist
can be directly sourced or copied. The .cjs
and .mjs
extensions for CommonJS and ES6 module imports respectively. The min.js
and umd.js
builds are minimized, with the former only being defined as a global import suitable for script tags in HTML.
Usage
The most basic use-case is to simply call the Templatize.render()
function.
var rendered = Templatize.render(myTemplate, bindings, options);
However this will not take advantage of template caching. If reusing the template, one can first create a rendering instance from said template using Templatize.from()
, then call the render function on that instance.
var templateOne = Templatize.from(myTemplate, options);
var rendered = templateOne.render(bindings);
# Templatize.render(template, bindings[, options])
| Name | Type | Description |
| --- | --- | :--- |
| template
| String | The template. |
| bindings
| Object | The object literal of data-bindings. |
| options
| Object | See options. |
Returns: The rendered template string.
# Templatize.from(template[, options])
Returns: An instance of the Templatize rendering interface based off this template.
# Interface.prototype.render(bindings[, options])
Options given here will overwrite those set when the interface was created – with one exception.
Templates and partials are parsed immediately with the custom delimiters (if supplied) in the function they were called. Custom delimiters supplied here have no effect on templates or partials supplied in Templatize.from()
. Likewise, custom delimiters must be defined here if needed for new partials also given here – even if the same custom delimiters given during interface creation.
Returns: The rendered template string.
Options
delimiters
- Set custom delimiters here as array of strings.errorOnFuncFailure
- If true, throw exceptions resulting from function calls in the data-bindings. Otherwise, simply warns in the console and returns empty for the binding being evaluated.evalZeroAsTrue
- If true, zero-values are treated as a real value for section evaluation. See section value evaluation.escapeAll
- If true, all tags are by default HTML special-character escaped. Any tag printing unescaped code needs the specific formatting directive. See formatting.errorOnMissingTags
- If true, throw exceptions when a data-binding called by the template is missing. Otherwise, simply warns in the console and returns empty.partials
- A map of partial templates by name. Used to refer to partials.
The Basics
Templates are strings in which tags define where the text will be dynamically replaced and updated. By default, tags use the double-curly-braces delimiters (e.g. {{likeThis}}
). The value inside the tag is the key or key name, which may be supplemented by special characters called directives that instruct special-case use or handling of the tag.
Whitespace between the delimiters and the inner key (and directives) are generally trimmed and ignored by the renderer, but as a general rule, either use no whitespaces or only between the delimiters and key, not within the key value itself -- e.g. {{likeThis}}
or {{ likeThis }}
but {{ not like this }}
.
The data-binding for a tag is the data, identified by the tag key, that will be supplanted in the tag's place.
Variables
Variables are the most basic use-case, where the tag will render the data-binding value associated with the tag's key. Dot-notation may be used to traverse the data-structure.
Template:
{{name.first}} is {{age}} years old.
Bindings:
{
name: { first: "Bob" },
age: 46
}
Outputs:
Bob is 46 years old.
The default behavior is to treat missing bindings as empty. You may also throw an exception when encounter a missing binding by setting the errorOnMissingTags
parameter in the render options.
Comments and escaping
Comments are done by placing a bang directive (!
) within the opening delimiter. For escaping, place a single backslash (\
) before the tag. A double backslash (\\
), however, will not be read as escaping the tag.
Template:
{{name.first}} is \{{age}} years old. {{! note to self: is this the right age? }}
Bindings:
{
name: { first: "Bob" },
age: 46
}
Outputs:
Bob is {{age}} years old.
Naming restrictions
Restrictions for tag key names
_display
is a special keyword. While it can be set (see the _display parameter), it should only be done when specifically calling said functionality.- Any key name starting with a reserved directive including:
- A leading bang (
!
) will be treated as a comment in the template code. - A leading period (
.
) will be treated as a context directive and not part of the key name. - A leading ampersand (
&
), hash (#
), or caret (^
) will be treated as one of the directives for lists and sections.
- A leading bang (
- Ending a key name with a semi-colon (
;
) will be interpreted as the escape formatting directive and not part of the key name. - Using in any place a double-colon (
::
), which is a formatting directive, or an arrow operator (->
), which is used for passing context to functions, will be interpreted as their respective directives.
Things to avoid in tag key names
- While whitespaces can be part of the key name, it is generally not good practice. At the very least avoid using it as leading or trailing characters. Templatize will generally handle trimming and adjust in cases where it does exist, but proper behavior cannot be fully guaranteed.
- While dots (
.
) can mostly be used in the key name without failing (though a few edge-cases may still result in odd behavior), it is generally to be avoided to reduce naming confusion.
Lists
Lists are marked with an ampersand (&
) and can take in an array (or a function that returns an array). The output is grammatically formatted with appropriate use of commas and/or the 'and'-conjunction, as dictated by the length of the list. No other dynamic text or subsections should be nested within a list, and values within the array should be strings or numbers only for best results.
One special case exists with the list functionality, the combination of the list and section directive (&#
) which can be used to grammatically list repeating sections.
Template:
{{&name}} sells {{&sells}} with his {{&with}}.
Bindings:
{
name: ["Bob"],
sells: ["burgers", "sodas", "fries"],
with: ["wife", "kids"]
}
Outputs:
Bob sells burgers, sodas, and fries with his wife and kids.
Note, the Oxford-comma is the default – and only – behavior, as the universe intended.
Sections
Section start at tags with the #
-directive and end at the corresponding tags with the /
-directive. If the data bound to the tag evaluates as true, the content between the section tags will be shown. Conversely, it will be hidden if it evaluates to false.
You may also inverse the rules for showing and hiding a section by replacing the hash (#
) with a caret (^
) in the section start tag.
Template:
Bob is {{#married}}married{{/married}}{{#single}}single{{/single}}.<br />
{{#spouse}}Bob is married to {{spouse}}.{{/spouse}}<br />
Bob has {{^haspets}}no pets{{/haspets}}{{#haspets}}pets{{/haspets}}.
Bindings:
{
married: true,
single: false,
spouse: "Linda",
haspets: false
}
Outputs:
Bob is married.
Bob is married to Linda.
Bob has no pets.
Section value evaluation
The data bound to a section tag is evaluated for 'truthiness'. Null values, an empty string or composed only of whitespace, an empty list, and 0
evaluate as false (though in certain cases you may want to treat 0-values as true). Otherwise, as long as data-binding for section evaluates to true, it will be treated as such. You may use this as a shortcut for both displaying the section and formatting its value.
Repeating Sections
If the value bound to a section tag is an array (or function that evaluates to an array), the section will be repeated for as many items as exists in the array.
Within the context of the repeating section (that is, between the opening and closing section tags), the same tag key is temporarily bound to the value of each item during each iteration. Thus, the tag key can be used within the section context to access the inner values as it iterates through the array.
Template:
{{#children}}Child: {{children}}<br />{{/children}}
Bindings:
{children: ["Tina", "Gene", "Louise", "", null, false, 0]}
Outputs:
Child: Tina
Child: Gene
Child: Louise
Note that each item is also treated to the same section value evaluation to determine whether it is rendered.
More on sections
See additional documentation for more on sections, including example sample design patterns, using the _display
parameter, treating zero values as true, and more.
Scoping and the context directive
All keys in template tags must provide the full path to the data-binding, even if within a section. However, one way to shortcut to the inner-most context is by prefacing the tag key with the context directive (.
). A naked context tag ({{.}}
) is particularly useful for repeating sections with flat values.
Template:
{{#name}}1. {{name.first}}{{/name}}<br />
{{#name}}2. {{first}}{{/name}}<br />
{{#name}}3. {{.first}}{{/name}}<br />
<br />
Friends: {{#friends}}{{.}} {{/friends}}
Bindings:
{
name: {first: "Bob"},
friends: ["Teddy", "Mort"]
}
Outputs:
1. Bob
2.
3. Bob
Friends: Teddy Mort
In the above, we try to access name.first
in three ways. Using the full binding path (1) works in almost any case. However, using first
without giving a context (2), fails as it tries to find a binding for first
from the root, which does not exist. We can fix this by providing the context directive (3), which begins the search from the given context, with is within the section (and corresponding data-binding for) name
.
The naked context tag ({{.}}
) in the final line is equivalent to the tag {{friends}}
, which in-context of a repeating section accesses each iterated value in the list.
Functions
Functions are evaluated and uses the returned value as the data-binding to the specified tag. As the behavior of the function depends on what is returned, it may be used in a variety of contexts, such as using the function output as a section or list.
The function is called within the context of the data-binding object where it resides (accessed via this
) and given the argument of the full/root data-binding object.
Template:
{{fullname}}'s friends include {{&friends}}.
Bindings:
{
name: {
first: "Bob",
last: "Belcher"
},
fullname: function(root) {
return this.name.first + " " + root.name.last;
},
relations: [
{name: "Teddy", friendly: true},
{name: "Mort", friendly: true},
{name: "Jimmy Pesto", friendly: false}
],
friends: function() {
return this.relations.filter(person => person.friendly)
.map(person => person.name);
}
}
Outputs:
Bob Belcher's friends include Teddy and Mort.
Note, since none of the tags were called in context, for the functions called, this
and root
will refer to the same (the root data-binding).
Error handling
By default, functions fail silently. If an error occurs during function call, exception is not raised further and value is assumed to be an empty string. To change this, simply set the errorOnFuncFailure
flag to true
in the options.
Passing context to functions
To change the context of a function (accessed by the this
keyword) when it is called, the tag may pair the key referencing a data context with the key for the function, using the pass-as-context directive (->
) to separate them. The function will also be passed a root
parameter that is always a reference to the data-binding at the top-most level.
The burger-of-the-day is:<br />
"{{specials->getTodays}}"
Bindings:
{
specials: {
sunday: "Yes I Cayenne Burger",
monday: "So Many Fennel So Little Thyme Burger"
},
today: "sunday",
getTodays: function(root) {
return this[root.today];
}
}
Outputs:
The burger-of-the-day is:
"Yes I Cayenne Burger"
This functionality is covered in greater depth in the additional function documentation under passing-context-to-functions.
More on functions
Functions are the most powerful aspect of Templatize, especially paired with the pass-context-to-function directive and chaining functions. However, errors can be difficult to debug without understanding how functions work. This section only covers the most basic use of functions.
See additional documentation for more on functions.
Formatting
Formatting options are also available by suffixing the key name in the template code with a double-colon (::
) and following with a format key. For strings, a few of the commonly recognized values are detailed in the below table. If not recognized, Templatize uses the format key as an input to the d3-format library, which handles many number formats. See documentation there for various formatting options.
- html - If the option
escapeAll
is set true, this key sets the output not to escape HTML special characters.- raw - Same as above.
- encode - Encodes HTML special characters in rendered output.
- upper - Transforms all alphabetical characters to uppercase.
- caps - Same as above.
- allcaps - Same as above.
- lower - Transforms all alphabetical characters to lowercase.
- capitalize - Capitalizes the first letter in each word.
Additionally, you can shorthand the encode format directive and key by suffixing a semi-colon (;
) to the end of the tag name. It may even be combined with another format directive.
Template:
{{name::capitalize}} lives in {{locale::capitalize}}
and sells burgers for {{price.burger::$.2f}}.
{{break}}
{{break::encode}}{{break;}}{{break::upper;}}
Bindings:
{
name: "bob",
locale: "new england",
price: { burger: 5 },
break: "<br />"
}
Outputs:
Bob lives in New England and sells burgers for $5.00.
<br /><br /><BR />
Formatting also works for lists and functions.
Template:
Order: {{&order::lower}}<br />
Prices: {{&ticket::$.2f}}<br />
Sale tax: {{salesTax::.0%}}<br />
Total: {{total::$.2f}}<br />
Total (w/ tax): {{addTax::$.2f}}
Bindings:
{
order: ["BURGER", "FRIES"],
prices: {
BURGER: 5,
FRIES: 2
},
ticket: function() {
return this.order.map(item => this.prices[item]);
},
salesTax: 0.05,
total: function() {
return this.order.reduce((total, item) => total + this.prices[item], 0);
},
addTax: function() {
return this.total()*(1+this.salesTax);
}
}
Outputs:
Order: burger and fries
Prices: $5.00 and $2.00
Sale tax: 5%
Total: $7.00
Total (w/ tax): $7.35
Partials
Partials are reusable sub-templates that can be called from the main template. Partials are supplied in the options and called in the template with the partial directive (>
).
The partial template will using the same data-bindings given for rendering the template. However, the in-context directive (.
) may be prefixed before the key name to render the partial with data-bindings corresponding to the current data context.
Partials cannot be used as sections or passed as context, but they can be given a formatting directive.
Template:
1. {{>fullname}}
{{#wife}}
2. {{>fullname::upper}}
3. {{>.fullname;}}
{{/wife}}
Bindings:
{
name: {
first: "Bob",
last: "Belcher"
},
wife: {
name: {
first: "Linda",
last: "Belcher"
}
}
}
Code:
var partials = {
fullname: "{{name.first}} {{name.last}}<br />"
};
Templatize.render(template, bindings, {partials: partials});
Outputs:
1. Bob Belcher
2. BOB BELCHER
3. Linda Belcher<br />
More Topics
The above only takes a cursory glance at some of the directives. Be sure to look into the additional documentation below.
Advanced usage, edge cases, and general weirdness
That's all great, you may be thinking, but what about if I pass a function to itself? Or use a pass-as-context directive in the section tag? What about multi-dimensional arrays? Did you think of all that?
Well luckily for you, you sadist, we have such a section on advanced usage, edge cases, and general weirdness.
Templatize vs Mustache.js
Time to address the elephant in the room. Why recreate what Mustache.js (basically) already does? How does Templatize differ? Which is better? Which is faster? The quick answers are: just because, much more powerful function directives (among a few other syntactic differences), depends what you want, and probably Mustache.js. But if you want a little more substance to those answers, see Templatize vs. Mustache.js.
Acknowledgments
Number formatting utilizes the d3-format module, which is Copyright under Mike Bostock. The full license can be found here.