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:

<div class="dialog dialog-{{ error.type }}">
  <h3 t:if="{{ error.type === 'error' }}">Error</h3>
  <h3 t:else>Warning</h3>
  <p>{{ error.message | capitalize }}</p>
  <button>ok</button>
</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:

{
    error: {
        type: "error",
        message: "Sorry no can do"
    }
}

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:

<img src="hi.jpg">
<br>
<ul>
   <li>Hello 
   <li>There
</ul>

Instead, do this:

<img src="hi.jpg"/>
<br/>
<ul>
   <li>Hello</li>
   <li>There</li>
</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:

{{ error.message }}

output:

Sorry no can do

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

{{ error.message | upcase }}

output:

SORRY NO CAN DO

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

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

is rendered as

Error: Sorry no can do.

operators

You can use all operators from the Liquid template language.

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

becomes

true

This is particularly useful when creating HTML with boolean attributes:

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

output:

<input type="radio" checked/>
<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:

{{ true or false and false }}

will evaluate to

true

because the or is evaluated first.

Standard Liquid Filters

All standard Liquid template filters are supported:

  • append - append a string e.g. {{ 'foo' | append:'bar' }} #=> 'foobar'
  • capitalize - capitalize words in the input sentence
  • ceil - rounds a number up to the nearest integer, e.g. {{ 4.6 | ceil }} #=> 5
  • date - format a date (syntax reference)
  • default - returns the given variable unless it is null or the empty string, when it will return the given value, e.g. {{ undefined_variable | default: "Default value" }} #=> "Default value"
  • divided_by - integer division e.g. {{ 10 | divided_by:3 }} #=> 3
  • downcase - convert an input string to lowercase
  • escape_once - returns an escaped version of html without affecting existing escaped entities
  • escape - html escape a string
  • first - get the first element of the passed in array
  • floor - rounds a number down to the nearest integer, e.g. {{ 4.6 | floor }} #=> 4
  • join - join elements of the array with certain character between them
  • last - get the last element of the passed in array
  • lstrip - strips all whitespace from the beginning of a string
  • map - map/collect an array on a given property
  • minus - subtraction e.g. {{ 4 | minus:2 }} #=> 2
  • modulo - remainder, e.g. {{ 3 | modulo:2 }} #=> 1
  • newline_to_br - replace each newline (\n) with html break
  • plus - addition e.g. {{ '1' | plus:'1' }} #=> 2, {{ 1 | plus:1 }} #=> 2
  • prepend - prepend a string e.g. {{ 'bar' | prepend:'foo' }} #=> 'foobar'
  • remove_first - remove the first occurrence e.g. {{ 'barbar' | remove_first:'bar' }} #=> 'bar'
  • remove - remove each occurrence e.g. {{ 'foobarfoobar' | remove:'foo' }} #=> 'barbar'
  • replace_first - replace the first occurrence e.g. {{ 'barbar' | replace_first:'bar','foo' }} #=> 'foobar'
  • replace - replace each occurrence e.g. {{ 'foofoo' | replace:'foo','bar' }} #=> 'barbar'
  • reverse - reverses the passed in array
  • round - rounds input to the nearest integer or specified number of decimals e.g. {{ 4.5612 | round: 2 }} #=> 4.56
  • rstrip - strips all whitespace from the end of a string
  • size - return the size of an array or string
  • slice - slice a string. Takes an offset and length, e.g. {{ "hello" | slice: -3, 3 }} #=> llo
  • sort - sort elements of the array
  • split - split a string on a matching pattern e.g. {{ "a~b" | split:"~" }} #=> ['a','b']
  • strip_html - strip html from string
  • strip_newlines - strip all newlines (\n) from string
  • strip - strips all whitespace from both ends of the string
  • times - multiplication e.g {{ 5 | times:4 }} #=> 20
  • truncate - truncate a string down to x characters. It also accepts a second parameter that will append to the string e.g. {{ 'foobarfoobar' | truncate: 5, '.' }} #=> 'foob.'
  • truncatewords - truncate a string down to x words
  • uniq - removed duplicate elements from an array, optionally using a given property to test for uniqueness
  • upcase - convert an input string to uppercase
  • url_encode - url encode a string

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

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"

Logic filters

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

  • is_falsy - if the input is falsy (= null 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 null.
  • else - if input is null, return argument. otherwise return input.

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

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

output:

Oh no!

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

<!-- this will produce an error: -->
<div t:if="{{ !user.custom.age }}">...</div> 

<!-- this will render the div if `age` is falsy (or does not exist) -->
<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:

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

output:

&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:

<div t:if="{{ error.type == 'error' }}">
    <h2>ERROR</h2>
    <p>{{ error.message }}</p>
</div>
<div t:else-if="{{ error.message contains 'Watch out' }}">
    <h3>Watch out</h3>
    <p>{{ error.message }}</p>
</div>
<div t:else>
    <p>{{ error.message }}</p>
</div>

output:

<div>
    <h2>ERROR</h2>
    <p>Sorry no can do</p>
</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:

{
    users: [
        {id: "a", name: "Pete"},
        {id: "b", name: "Alice"},
        {id: "c", name: "Kerem"}
    ]
}

Display an element for each entry in users as follows:

<p t:for="{{ user in users }}" t:key="{{ user.id }}">
  {{ user.name }}
</p>

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

output:

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

<p t:key="a">1. Pete</p>
<p t:key="b">2. Alice</p>
<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:

{
    userMap: {
        a: {name: "Pete"},
        b: {name: "Alice"},
        c: {name: "Kerem"}
    }
}

D

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

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

output:

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

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:

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

output:

<p t:key="b">Alice</p>
<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:

<t:set shortBigName="{{user.name | upcase | truncate: 20}}"/>
<div t:if="{{ shortBigName == 'ALICE'}}">
    Hi Alice!
</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:

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

Note that the following shorthand syntax accomplishes the same:

<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:

{{ 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:

<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:

<h{{ section.level }}>Section {{ section.index }}</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.