Liquid

Liquid

A widget’s template is HTML with Liquid mixed in. Liquid is a small template language: it writes values into the page, chooses between pieces of HTML and repeats them. Tiledash uses LiquidJS, so what you find in Liquid’s own documentation mostly works here too. This page covers what you will use most, and what is different.

Writing a value

Two curly braces write a value into the page:

<p>{{ device.name }}</p><p>{{ cap.measure_power.value }} W</p>

Everything that comes out is escaped. A device named <b>Lamp</b> shows those characters as text instead of making the word bold, and a template can’t be tricked into adding elements through a name or a reading. The raw filter exists in Liquid, but in a widget it changes nothing: the output is escaped anyway.

A value that doesn’t exist writes nothing, and doesn’t cause an error. {{ cap.dim.value }} on a device without brightness is simply empty, so use the default filter or an {% if %} where you need something to show.

The context reference lists everything you can read.

Filters

A filter changes a value. Write it after a vertical bar, and add more by chaining them:

{{ cap.measure_temperature.value | round: 1 }}{{ device.name | upcase | truncate: 12 }}{{ cap.dim.value | percent }}

Some filters take arguments after a colon, separated by commas. A filter that doesn’t exist is an error when you save, so a typo can’t get through.

Tiledash adds filters made for readings:

FilterWhat it doesExample result
round: 1Rounds a number21.456 becomes 21.5
decimals: 1Writes exactly this many decimals, 0 to 321 becomes 21.0
percentA value from 0 to 1 as a percentage0.62 becomes 62 %
unit: 'W'A value with its unit, scaled1820 becomes 1.82 kW
tempA temperature with a degree sign21.46 becomes 21.5°
on_offtrue or false as On or Offtrue becomes On
relative_timeHow long ago a time wasA time becomes 4 min ago
durationSeconds as hours and minutes5400 becomes 1 h 30 min
quoteA value as one quoted text, for handlersLamp becomes "Lamp"

The usual Liquid filters work too, such as default, append, replace, split, join, first, last, size, plus, minus, times, at_least, at_most and date. The filters reference lists them all.

Filters are handy for small things. When you need to calculate with several values, an expression reads better than a chain of plus and times.

Conditions

{% if %} shows a piece of HTML only when something holds:

{% if cap.onoff.value %}  <span class="badge">On</span>{% elsif device.available == false %}  <span class="badge">Offline</span>{% else %}  <span class="badge">Off</span>{% endif %}

{% unless %} is the opposite of {% if %}, and {% case %} chooses between several values of one thing:

{% case cap.thermostat_mode.value %}  {% when 'heat' %}Heating  {% when 'cool' %}Cooling  {% else %}Idle{% endcase %}

You can compare with ==, !=, >, <, >= and <=, combine conditions with and and or, and test whether a reading exists by naming it: {% if cap.dim %} is true for a device that has brightness.

Liquid has its own idea of true and false that surprises people. Only false and nothing at all count as false. The number 0 and empty text are both true, so {% if cap.measure_power.value %} is true while the power is 0. Compare with a number when you mean it: {% if cap.measure_power.value > 0 %}.

Liquid’s blank stands for nothing or empty text, which is useful for settings the tile left empty: {% if settings.title != blank %}.

Conditions that Liquid can’t read, such as {% if until(t, 'days') < 2 %}, are read as expressions instead.

Variables

{% assign %} gives a value a name for the rest of the template, and {% capture %} collects a piece of output as text:

{% assign watts = cap.measure_power.value %}{% capture label %}{{ device.name }} · {{ device.zone }}{% endcapture %}<p title="{{ label }}">{{ watts | unit: 'W' }}</p>

{% let %} does the same job as assign for expressions.

Loops

{% for %} repeats a piece of HTML for each item of a list:

<ul>  {% for r in settings.lines.items %}    <li>{{ r.label }}: {{ r.cap.text }} {{ r.cap.unit }}</li>  {% else %}    <li>Nothing to show yet</li>  {% endfor %}</ul>

The {% else %} part is used when the list is empty or missing.

Inside the loop, forloop knows where you are: forloop.index counts from 1, and forloop.first and forloop.last are true for the first and last item. limit and offset take a part of the list, reversed turns it round, and {% break %} and {% continue %} leave early or skip an item:

{% for r in settings.lines.items limit: 3 %}  {{ r.label }}{% unless forloop.last %}, {% endunless %}{% endfor %}

You can also loop over a range of numbers, {% for i in (1..5) %}, or over everything a device can report:

{% for entry in cap %}  {{ entry[1].title }}: {{ entry[1].text }} {{ entry[1].unit }}{% endfor %}

When you loop over an object such as cap, each round gives you a pair: entry[0] is the capability’s id and entry[1] is its readings.

Partials

{% render %} draws another piece of template, a partial, with the values you give it:

{% render 'value-row', label: 'Power', value: cap.measure_power.text, unit: cap.measure_power.unit %}

A partial you installed from the store is rendered by its id: {% render '@ann/sprinkler', on: cap.onoff.value %}. A partial declares the parameters it takes, each with a type; a render that leaves out a required one, or writes out a value of the wrong type, is an error when you save, and one that leaves out an optional one gives the partial its default. A partial sees only what a render gives it; one that declares device gets the tile’s own device when a render leaves it out. The Partials page explains them.

Comments and spacing

{% comment %}…{% endcomment %} leaves a note in the template that is never drawn. For a short one, {% # like this %} works as well. {% raw %} writes its content as it is, curly braces included.

A dash on the inside of a tag, {%- or -%}, removes the spaces and line breaks next to it. Use it where an extra space would show:

<span class="big">  {%- if on -%}On{%- else -%}Off{%- endif -%}</span>

What isn’t available

A widget can’t include files or inherit from another template, so there are two tags you can’t use:

  • {% include %} and {% layout %} are refused with a message when you save. Use {% render %} with a partial instead.
  • {% render %} takes the name of a partial in quotes (or a store partial’s id, '@ann/sprinkler') and its values as key: value pairs. A name taken from a variable, the for and with forms of render, and filters on a value you give it aren’t supported: assign the value first.

Templates never read files, and a template can’t be longer than 20,000 characters. A template that does too much work is stopped, so a mistake can’t hang a dashboard. Work is counted in steps: every tag, value and piece of text drawn, every round of a loop and everything a partial draws. A template may take 50,000 steps, more than thirty times what the heaviest built-in widget needs. Because it counts work rather than time, a template that passes when you save it draws the same on a fast laptop, a slow wall panel or a busy server. A loop that builds an enormous page is stopped as well.

Escaping and handlers

Because every value is escaped, it is safe to put device names and readings anywhere in your HTML, including inside an attribute:

<button aria-label="Switch {{ device.name }}">…</button>

The one place to be careful is a handler. A handler is code, and a value that becomes part of it must stay a single piece of text. The quote filter does that: it writes the value as one quoted text that can’t add anything to the line.

<button data-hd-on-click="openHistory({{ id | quote }})">History</button>

Read Taps and handlers for more.