Template Language Reference

TalkJS uses a custom template language. It borrows heavily from the Liquid Markup language and from Vue.js templates.

  • Like Liquid, it is designed for fast and secure end-user programming, meaning it does not allow custom JavaScript code.
  • Like Vue and React, it is designed to enable performant client-side rendered UI components.

A template is a piece of HTML with special attributes and special {{ expressions }}. A representative example:

1<div class="dialog dialog-{{ error.type }}">
2 <h3 t:if="{{ error.type === 'error' }}">Error</h3>
3 <h3 t:else>Warning</h3>
4 <p>{{ error.message | capitalize }}</p>
5 <button>ok</button>
6</div>

Templates are normal HTML files with two special features:

  • Expressions, eg {{ error.type }}
  • Directives, eg t:if=".." and <t:set ../>

For the remainder of this document, our examples assume we're rendering data structured like this:

1{
2 error: {
3 type: "error",
4 message: "Sorry no can do"
5 }
6}

HTML limitations

Unlike normal HTML, TalkJS templates must be well-structured XML. Notably, every opening tag needs to have a corresponding closing tags, and tags that have no content must be self-closing, e.g. <br/>.

This is invalid:

1<img src="hi.jpg">
2<br>
3<ul>
4 <li>Hello
5 <li>There
6</ul>

Instead, do this:

1<img src="hi.jpg"/>
2<br/>
3<ul>
4 <li>Hello</li>
5 <li>There</li>
6</ul>

The TalkJS template language is similar to React's JSX and Vue templates in this regard.

Expressions

Expressions are valid inside attributes or in text nodes, and you typically use them to display text from the data a template is rendered with. For example, displaying text or a number is simply:

1{{ error.message }}

output:

1Sorry no can do

You can modify data using filters, separated by a pipe. A simple helper looks like this:

1{{ error.message | upcase }}

output:

1SORRY NO CAN DO

You can chain filters by adding more. Also, some filters take an argument:

1{{ error.message | t whatever : "Error: " | append: "."}}

is rendered as

1Error: Sorry no can do.

operators

You can use all operators from the Liquid template language.

1{{ error.type == 'error' }}

becomes

1true

This is particularly useful when creating HTML with boolean attributes:

1<input type="radio" checked="{{ error.type == 'error' }}"/>
2<input type="radio" checked="{{ error.type == 'warning' }}"/>

output:

1<input type="radio" checked/>
2<input type="radio"/>

Order of operations

Expressions with more than one and or or operator are interpreted from right to left. Brackets are not valid inside expressions, so you cannot change the order of operations using brackets. For example:

1{{ true or false and false }}

will evaluate to

1true

because the or is evaluated first.

Standard Liquid Filters

All standard Liquid template filters are supported:

FilterDescription
appendappend a string e.g.
1{{ 'foo' | append:'bar' }} #=> 'foobar'
at_leastLimits a number to a minimum value. e.g.
1{{ 3 | at_least:4 }} #=> 4
at_mostLimits a number to a maximum value. e.g.
1{{ 3 | at_most:2 }} #=> 2
capitalizecapitalize words in the input sentence
ceilrounds a number up to the nearest integer, e.g.
1{{ 4.6 | ceil }} #=> 5
dateformat a date (syntax reference)
defaultreturns the given variable unless it is nil or the empty string, when it will return the given value, e.g.
1{{ undefined_variable | default: "Default value" }} #=> "Default value"
divided_byinteger division e.g.
1{{ 10 | divided_by:3 }} #=> 3
downcaseconvert an input string to lowercase
escape_oncereturns an escaped version of html without affecting existing escaped entities
escapehtml escape a string
firstget the first element of the passed in array
floorrounds a number down to the nearest integer, e.g.
1{{ 4.6 | floor }} #=> 4
joinjoin elements of the array with certain character between them
lastget the last element of the passed in array
lstripstrips all whitespace from the beginning of a string
mapmap/collect an array on a given property e.g.
1{{ users | map: "name"}} #=> ["Alice", "Bob"]
minussubtraction e.g.
1{{ 4 | minus:2 }} #=> 2
moduloremainder, e.g.
1{{ 3 | modulo:2 }} #=> 1
newline_to_brreplace each newline (\n) with html break
plusaddition e.g.
1{{ '1' | plus:'1' }} #=> 2`
,
1{{ 1 | plus:1 }} #=> 2
prependprepend a string e.g.
1{{ 'bar' | prepend:'foo' }} #=> 'foobar'
remove_firstremove the first occurrence e.g.
1{{ 'barfoobar' | remove_first:'bar' }} #=> 'foobar'
remove_lastremove the last occurence e.g.
1{{ 'barfoobar' | remove_last:'bar' }} #=> 'barfoo'
removeremove each occurrence e.g.
1{{ 'foobarfoobar' | remove:'foo' }} #=> 'barbar'
replace_firstreplace the first occurrence e.g.
1{{ 'barfoobar' | replace_first:'bar','foo' }} #=> 'foofoobar'
replace_lastreplace the last occurence e.g.
1{{ 'barfoobar' | replace_last:'bar', 'foo' }} #=> 'barfoofoo'
replacereplace each occurrence e.g.
1{{ 'foofoo' | replace:'foo','bar' }} #=> 'barbar'
reversereverses the passed in array
roundrounds input to the nearest integer or specified number of decimals e.g.
1{{ 4.5612 | round: 2 }} #=> 4.56
rstripstrips all whitespace from the end of a string
sizereturn the size of an array or string
sliceslice a string. Takes an offset and length, e.g.
1{{ "hello" | slice: -3, 3 }} #=> llo
sortsort elements of the array
splitsplit a string on a matching pattern e.g.
1{{ "a~b" | split:"~" }} #=> ['a','b']
strip_htmlstrip html from string
strip_newlinesstrip all newlines (\n) from string
stripstrips all whitespace from both ends of the string
timesmultiplication e.g
1{{ 5 | times:4 }} #=> 20
truncatetruncate a string down to x characters. It also accepts a second parameter that will append to the string e.g.
1{{ 'foobarfoobar' | truncate: 5, '.' }} #=> 'foob.'
truncatewordstruncate a string down to x words
uniqremoved duplicate elements from an array, optionally using a given property to test for uniqueness
upcaseconvert an input string to uppercase
url_encodeurl encode a string
whereFilters an array to only include objects with a given property value, or any truthy value by default. e.g.
1{{ users | where:"role", "admin" }} #=> [{"name": "Alice", "role": "admin"}]

All of these filters have been taken verbatim from Liquid Templates and are documented in more detail on the Liquid website.

where and map support looking up a key deeper in objects while iterating through arrays. For instance

1{{ users | map: "custom.displayName" }}

Will return an array, where for each user, it finds a property named custom, and on that object, finds a property named displayName. If either of these properties are not defined, then the property is evaluated as nil.

This works similarly with where:

1{{ users | where: "custom.visibility", "visible" }}

This will return an array of users where custom.visibility evaluates to "visible".

TalkJS-specific filters

  • random_color - picks a random color (in CSS color format, eg #ff0088) for the given string. The same input always results in the same color.
  • initials - extracts initials from a name. Supports Latin and Cyrillic scripts, returns the first character of the entire string for any other scripts. Eg "Alice von Smith" becomes "AvS", "Иван Лобов" becomes "ИЛ".
  • filesize - converts a number of bytes into a human-readable file size string, eg "2 KB" or "251 MB"
  • format_duration - converts a number of seconds into a MM:SS or HH:MM:SS string, eg "00:26" or "01:04:43"

Logic filters

In addition, four filters are supported that allow rudimentary logic inside expressions:

  • is_falsy - if the input is falsy (= nil or false), return true
  • is_truthy - if the input is truthy (= any other value), return true
  • then - if input is truthy, return argument. otherwise return nil.
  • else - if input is nil, return argument. otherwise return input.

Combined, then and else can be used as an inline conditional expression like this:

1{{ error.type === "error" | then: "Oh no!" | else: "Watch out" }}

output:

1Oh no!

is_falsy is particularly useful in t:if directives, which does not (currently) have a "not" operator:

1<!-- this will produce an error: -->
2<div t:if="{{ !user.custom.age }}">...</div>
3
4<!-- this will render the div if `age` is falsy (or does not exist) -->
5<div t:if="{{ user.custom.age | is_falsy }}">...</div>

Limitations

You cannot emit or modify HTML tags using expressions, because then the template isn't valid HTML anymore. If you try to use expressions to create HTML tags, you'll instead see quoted text:

1{{ user.customCssUrl | prepend: "<link href='" | append: "'>" }}

output:

1&lt;link href='myfile.css'&gt;

Directives

All directives begin with a t: to help distinguish them from normal attributes.

If/else

You can use t:if, t:else-if and t:else to render different HTML based on expressions:

1<div t:if="{{ error.type == 'error' }}">
2 <h2>ERROR</h2>
3 <p>{{ error.message }}</p>
4</div>
5<div t:else-if="{{ error.message contains 'Watch out' }}">
6 <h3>Watch out</h3>
7 <p>{{ error.message }}</p>
8</div>
9<div t:else>
10 <p>{{ error.message }}</p>
11</div>

output:

1<div>
2 <h2>ERROR</h2>
3 <p>Sorry no can do</p>
4</div>

If an t:if expression returns a falsy value, the entire element along with all its children will be omitted from the output.

Iteration

You can iterate over lists of data using t:for. Imagine a list of user data defined as follows:

1{
2 users: [
3 {id: "a", name: "Pete"},
4 {id: "b", name: "Alice"},
5 {id: "c", name: "Kerem"}
6 ]
7}

Display an element for each entry in users as follows:

1<p t:for="{{ user in users }}" t:key="{{ user.id }}">
2 {{ user.name }}
3</p>
4
5<!-- alternative syntax, if you need the index: -->
6<p t:for="{{ (user, index) in users }}" t:key="{{ index }}">
7 {{ index | plus: 1 }}. {{ user.name }}
8</p>

output:

1<p t:key="a">Pete</p>
2<p t:key="b">Alice</p>
3<p t:key="c">Kerem</p>
4
5<p t:key="a">1. Pete</p>
6<p t:key="b">2. Alice</p>
7<p t:key="c">3. Kerem</p>

To ensure that your app performs well, you must produce a t:key attribute with a unique value for each entry. Choose a value that doesn't usually change over time, such as an ID field or something like that.

Iteration over objects

You can iterate over objects, or maps, of data with the same syntax. Imagine the case where our user data was structured as follows:

1{
2 userMap: {
3 a: {name: "Pete"},
4 b: {name: "Alice"},
5 c: {name: "Kerem"}
6 }
7}

You can iterate over each member of the object like this:

1<p t:for="{{ (user, id) in userMap }}" t:key="{{ id }}">
2 {{ user.name }}
3</p>

output:

1<p t:key="a">Pete</p>
2<p t:key="b">Alice</p>
3<p t:key="c">Kerem</p>

The forloop object

Inside a t:for, a special variable called forloop is set, with these elements:

FieldTypeDescription
forloop.firstbooleantrue if this is the first item in the list
forloop.lastbooleantrue if this is the last item in the list
forloop.indexnumber0-based index of the current item in the list (0-based)
forloop.rindexnumber0-based index of the current item list, when counting down from the end
forloop.lengthnumbertotal amount of items in the list
forloop.parentloopobjectwhen in a nested loop, this is the forloop variable of the outer loop

For example:

1<span t:for="{{ user in userMap }}" t:key="{{ user.id }}">
2 {{ user.name }}<span t:if="{{ forloop.last == false }}">, </span>
3</span>

output:

1<span>Pete<span>, </span></span>
2<span>Alice<span>, </span></span>
3<span>Kerem</span>

Note that index, rindex and length are always numeric, also when iterating over the keys of an object.

Combining for with if

If an element has both an t:if and a t:for attribute, the t:if will be evaluated for every element of the list. This lets you make filtered lists:

1<p
2 t:for="{{ (user, index) in users }}"
3 t:if="{{ index > 0 }}" t:key="{{ user.id }}"
4>
5 {{user.name}}
6</p>

output:

1<p t:key="b">Alice</p>
2<p t:key="c">Kerem</p>

Setting variables

If you wish to reduce code duplication, or if you need to use t:if/t:else-if with complex expressions, you may want to define your own variables. This is done with the special <t:set/> element, which adds values to the current scope.

For example:

1<t:set shortBigName="{{user.name | upcase | truncate: 20}}"/>
2<div t:if="{{ shortBigName == 'ALICE'}}">
3 Hi Alice!
4</div>

Variables set using t:set inside a t:for are not available outside it. You can set multiple variables inside a single t:set element if you want. A t:set element produces no output.

Conditional assignment

You can combine t:set with t:if to conditionally set a variable:

1<t:set t:if="{{ user.age < 12 }}" age="young"/>
2<t:set t:else age="old"/>

Note that the following shorthand syntax accomplishes the same:

1<t:set age="{{ user.age < 12 | then: 'young' | else: 'old' }}"/>

Why a new template language?

The industry has largely standardized on Liquid Templates for customer programmable web UIs. Why, then, do we insist on inventing our own? Here's some technical background.

Liquid templates are designed for use in server-side rendered websites. For pages that, in practice, aren't all that interactive. TalkJS, like many other modern applications, is designed as a single-page web app (or, more precisely, as a single-page web component). We ReactJS for its UI. A lot of things can change dynamically inside a chat UI: messags can be added, "marked as read" ticks can appear, messages can be edited, conversations can switch, the timestamp can update, and so on.

Imagine that we'd use Liquid templates to allow customers to customize the look and feel of, say, message balloons. Then our frontend would need to rerender the entire HTML result of their templates every time something changes. This results in bad performance, high battery usage, bad UX and, possibly, a flickery UI.

Instead, we want a template language that's as easy as Liquid Templates, but easier to use in the context of frameworks like React or Vue. One that we can convert into React components on the fly, instead of a big fat string of HTML.

The key benefit of Liquid over other template languages is that Liquid templates don't allow customers to write arbitrary JavaScript or Ruby code, but only a limited set of operations. This is key for security and forward compability, so we need to keep that. To the best of our knowledge, there is no template system out there that combines these two goals, so we built one.

Differences with Liquid

We tried hard not to re-invent the wheel. We imported the Liquid expression syntax verbatim, including all filters:

1{{ user.name | upcase | truncate: 20 }}

To be able to use boolean attributes such as disabled and checked easily, we added the ability to evaluate boolean expressions:

1<button disabled="{{ user.age < 16 }}">Watch scary move</button>

The above is not valid in Liquid templates.

Liquid tags

{% tags %} are a key Liquid feature. We don't support them at all, because they make it extremely hard to convert a template to React components. For example, this is a valid Liquid template:

1<h{{ section.level }}>
2 Section {{ section.index }}
3</h{{ section.level }}>

It's a very powerful feature, but nigh on impossible to parse as HTML. Instead, we found that the Vue designers came up with an excellent template syntax that is much easier to represent as an in-memory component structure, which is almost as powerful.

Differences with Vue

Vue.js is an excellent framework for building single-page web applications. It includes a template language which itself must be valid HTML, except for a few magic attributes. This not only makes it extremely easy to parse, it also guarantees that a template can be mapped one-on-one to a component structure. We took that idea verbatim from Vue.

We also cherry-picked core Vue directives, such as v-if and v-for, except that they're renamed to t:if and t:for. We took their exact syntax and semantics, except that expressions aren't JavaScript like in Vue templates, but Liquid-style expressions.

Like Vue templates and React's JSX syntax, we strip all whitespace between HTML elements and the start/end of the line, to avoid rendering dummy whitespace to the UI.

Future developments

We plan to open-source this language in the near future and we plan to keep making it more powerful.