CEL
Expressions are written in the Common Expression Language (CEL), a small language designed for evaluating untrusted expressions quickly and safely. It looks like the expression part of C, Java or JavaScript:
s.count + 1
s.user.name + ' (' + string(s.user.unread) + ')'
s.saved ? 'success' : 'failure'
size(s.items) == 0
CEL has no side effects and is not Turing-complete: evaluating an expression cannot change signals or send messages, and its cost is bounded by the size of the expression and its input. It has independent implementations in Go, Java, C++, Rust, JavaScript, Python and others.
Syntax
An expression is a JSON object with a single cel member holding the
expression as a string. It takes the place of a literal value:
element {"id":"static","type":"text","text":"Hello"}
element {"id":"computed","type":"text","text":{"cel":"'Hello, ' + s.user.name"}}
Any other value is literal, including objects with other members or with a non-string
cel.
"text": {"cel":"'Count: ' + string(s.count)"}
"visible": {"cel":"s.count > 0"}
"disabled": {"cel":"s._busy || s.login.user == ''"}
"variant": {"cel":"s.saved ? 'success' : 'failure'"}
The s variable
Expressions have a single variable, s, which holds the current signals. With
the signals {"count":3,"user":{"name":"Ada"},"_open":true}, the expressions
s.count, s.user.name and s._open evaluate to
3, "Ada" and true. Keys that are not CEL
identifiers can be read with an index, as in s['first-name'].
Where expressions are allowed
Any attribute may be an expression except the structural ones, which must be literal:
id, type, parent, before,
bind and the handlers (onPress, onChange,
onSubmit, onDismiss). The
component reference marks each attribute that accepts an
expression.
Inside a handler, the leaf values of a
patch may be expressions. They are evaluated against the current signals
before the patch is applied, which allows a toggle:
"onPress": {"patch":{"_expanded":{"cel":"!s._expanded"}}}
Functions
Every renderer provides the CEL standard definitions and the strings extension, listed
below. Functions written x.f() are called on a value; the others are called
on their own or through their namespace.
Standard
Defined in the CEL language definition.
| Operator or function | Description |
|---|---|
! && || |
Logical not, and, or. && and || tolerate an error
on one side if the other side decides the result.
|
c ? a : b |
Conditional. |
== != < <=
> >=
|
Comparison. Numbers of different types compare by value. |
+ - * / % |
Arithmetic on numbers of the same type. + also concatenates strings and
lists. - also negates.
|
a in b |
Whether a list contains a value, or a map has a key. |
a[i] |
List index or map key. |
size(x), x.size() |
Length of a string (in code points), list or map. |
str.contains(t) |
Whether str contains t. |
str.startsWith(t), str.endsWith(t) |
Prefix and suffix tests. |
str.matches(re), matches(str, re) |
Whether str contains a match of the
RE2 regular expression.
|
int(x), uint(x), double(x) |
Number conversions, from numbers or strings. |
string(x) |
Converts a number, bool, timestamp or duration to a string. |
bool(x) |
Parses "true" or "false". |
bytes(x) |
Converts a string to bytes. |
dyn(x) |
Marks a value as dynamically typed. |
type(x) |
The type of a value, comparable with int, string,
list, map and so on.
|
timestamp(str), duration(str) |
Parse an RFC 3339 timestamp, or a duration such as "1h30m". Timestamps
and durations support +, - and comparison.
|
t.getFullYear(), getMonth(), getDate(),
getDayOfMonth(), getDayOfWeek(),
getDayOfYear(), getHours(), getMinutes(),
getSeconds(), getMilliseconds()
|
Timestamp components, in UTC or in the time zone given as an argument.
getHours() to getMilliseconds() also apply to durations.
|
Macros:
| Macro | Description |
|---|---|
has(x.f) |
Whether the map x has the key f: has(s.user).
|
l.all(x, p) |
Whether p holds for every item. On a map, iterates over keys. |
l.exists(x, p) |
Whether p holds for some item. |
l.exists_one(x, p) |
Whether p holds for exactly one item. |
l.map(x, e), l.map(x, p, e) |
The list of e for each item, optionally only those for which
p holds.
|
l.filter(x, p) |
The items for which p holds. |
Strings
Defined by the cel-go strings extension. Indexes count code points.
| Function | Description |
|---|---|
str.charAt(i) |
The character at index i, or "" at the end of the string.
|
str.indexOf(t), str.indexOf(t, from) |
Index of the first occurrence of t, or -1. |
str.lastIndexOf(t), str.lastIndexOf(t, from) |
Index of the last occurrence of t, or -1. |
str.substring(start), str.substring(start, end) |
The characters from start up to end, exclusive. |
str.lowerAscii(), str.upperAscii() |
Changes the case of ASCII letters only. |
str.trim() |
Removes leading and trailing whitespace. |
str.replace(old, new), str.replace(old, new, n) |
Replaces occurrences of old, all of them or the first n.
|
str.split(sep), str.split(sep, n) |
Splits into a list, into at most n parts if given. |
l.join(), l.join(sep) |
Joins a list of strings. |
str.reverse() |
The characters in reverse order. |
str.format(args) |
Formats a list of values, printf style:
"%s has %d items".format([s.name, size(s.items)]). Verbs:
%s, %d, %f (with precision, as
%.2f), %e, %b, %o,
%x, %X, %%.
|
strings.quote(str) |
The string as a quoted CEL string literal. |
Types
Signals are JSON, and are seen by CEL as follows:
| JSON | CEL |
|---|---|
| null | null_type |
| true, false | bool |
| number with no fraction or exponent | int |
| any other number | double |
| string | string |
| array | list(dyn) |
| object | map(string, dyn) |
CEL does not mix int and double in arithmetic. If a signal can
hold either, convert it first: double(x) * 2.0.
The result must match the attribute's type. String and enum attributes need a
string, with no implicit conversion, so numbers must go through
string() or format(). Bool attributes need a bool.
Number attributes accept int, uint or double.
Failure
If an expression fails to parse, fails to evaluate, or produces a value of the wrong type, the attribute takes its default. The renderer reports this as a diagnostic, never to the user.
Reading a signal that does not exist is an evaluation failure. The default of
visible and open is true, so a failed expression
shows the element. Send signals before the elements that read them, or test for them with
has():
"text": {"cel":"has(s.user.name) ? s.user.name : 'guest'"}
The exact rules are in the specification.