Dynamic expressions
- 11-12 minutes to read
The UI Framework implements the concept of self-updating configurations where, instead of passing a value, you can put a reference to, say, the value of the option, and each time this option is updated, the value is overwritten. This appears as follows:
{
"title": "{{ $['corner-type'].selected.title }}"
}
Here, the value of the title
property is obtained at runtime. This element corresponds to the title of a currently selected value in the widget called corner-type
.
If you are familiar with HTML template engines, like Razor, Jade, Mustache, etc., you may find it very similar. The only difference is that it is applied to JSON, not HTML. However, in the UI Framework, template engine capabilities are quite standard. It supports:
- Getting a value
- Conditional expressions
- Loops
- Variables
- Merge/concat objects
What parts of the editor config can be dynamic?
At the moment, this only works with widget parameters. You can use dynamic params as follows:
{
"widgets": [{
"type": "static-text",
"name": "team",
"params": {
"text": "{{ $['team-selector'].selected.title}}"
}
}]
}
However, if you try to write the dynamic part outside of the params
, it won't work. The following example does not work:
{
"widgets": [{
"type": "static-text",
"name": "team",
"title": "{{ $['team-selector'].selected.title}}",
"params": {
...
}
}]
}
Note
You can not reference a widget within the same widget. However, you may use the self
variable inside the expression.
How does this work?
When getting the config, the editor looks for dynamic parts in the config. This can be one of:
- A string value containing double curly braces, for example,
'something': '{{js-code}}'
- An object containing a key with double curly braces, for example,
'something': {'{{#if condition}}': 'value'}
The editor executes the code inside braces or special expressions starting with #
, and then replaces the dynamic part with the result. Also, it spots the parts that can be changed at runtime, such as references to variables or widgets, and subscribes to their changes. When such changes occur, it reassigns the corresponding values in the config.
Syntax
Getting the Value
{{ <some JS code> }}
In the double curly braces, you can write any valid JS code. For practical use, you need access to some data. Dynamic Configs provide the following entities:
$
- the global scope where you can get to the widgets (as a key-value dictionary). For example,$['widget-name']
provides an object from which you can read the same properties as the editor reads.main
- an au-wizard object reflecting the editor.vars
- config variables.self
- refers to the values of the current widget.product
,order
,user
- objects from the e-commerce driver.driver
- an e-commerce driver.product
- the loaded product, an object from the e-commerce driver.order
- an order or the first item from the cart, an object from the e-commerce driver.cart
- a cart.user
- info about the current user.settings
- plugin settings.sessionGuid
- a unique string generated for each launch of the editor.getOrDefault(expression, default)
- a helper method that evaluates theexpression
and returns its result if everything is ok, ordefault
if an error occurs. Here,expression
is a string that must be wrapped with\"
, for example,"{{ getOrDefault(\"$['gift-card'].proofImageUrls[0][0]\", '') }}"
.
All widgets that have a selected item, support a _
shortcut. In other words,
{{ root.$['option'].selected.title}}
and
{{ root.$['option']._.title}}
are equivalent.
Conditional expressions (#if/#elseif/#else)
"something": {
"{{ #if <expression 1>}}": "value-if-true",
"{{ #elseif <expression 2>}}": "otherwise-if-this-true",
"{{ #else }}": "value-if-everything-is-false"
}
If expression 1
is true, then something
becomes value-if-true
. If expression 2
is true, then something
becomes otherwise-if-this-true
. Otherwise, it becomes value-if-everything-is-false
.
You can omit #elseif
, #else
, or both. In the case when you omit the #else
clause, and the conditions are not met, then undefined
will be returned (the same as removing the key from the config).
As expressions, you can also use any JS code that returns a Boolean value, for example, widget values, variables, and so on.
You can return not only string values but also objects and even other expressions.
Loop (#each as)
"something": {
"{{ #each <an array> as item }}": {
"foo": "{{item.bar}}"
}
}
Here, something
becomes an array formed from element values. In this expression, you can refer to the passed array by using the name specified after as
.
To make it clearer, let's refer to the following example:
"names": {
"{{ #each [{first: 'John', last: 'Lennon'}, {first: 'Paul', last: 'McCartney'} as person ] }}": "{{ person.first + ' ' + person.last }}"
}
This expression returns the following array:
"names": [ "John Lennon", "Paul McCartney" ]
The next example illustrates how you can create an array of objects:
"names": {
"{{ #each [{id: 0, name: 'Chicago Bears'}, {id: 1, name: 'Washington Redskins'} as team ] }}": {
"id": "{{ team.id }}",
"title": "{{ team.name }}",
"price": 0
}
}
This expression results in the following array:
"names": [
{
"id": 0,
"title": "Chicago Bears",
"price": 0
},
{
"id": 1,
"title": "Washington Redskins",
"price": 0
}
]
Inside the loop, in addition to referencing the current list item, you can also get its index (a serial number) with the index variable:
{
"{{#each ['John', 'Paul', 'George', 'Ringo']}}": {
"{{`Name${index+1}`}}": "{{item}}"
}
}
This expression results in the following array:
[ {"Name1": "John"}, {"Name2": "Paul"}, {"Name3": "George"}, {"Name4": "Ringo"} ]
Cycles can be extremely useful together when used with the #flatten
operator.
Flattening arrays in objects (#flatten)
Say, we want to create an object of this type:
{
"Team 1": { "name": "Washington Capitals" },
"Team 2": { "name": "Vegas Golden Knights" },
"Team 3": { "name": "Pittsburg Penguins" },
"Background": "nhl1-bg.jpg",
"Heading": "2019 Stanley Cup® Championship"
}
However, we do not know in advance how many teams we will have. Another difficulty is that #each
returns an array of objects, but we do not only need an object. This object must be combined with another object containing data that does not depend on commands.
Here, the #flatten
operator comes into play. It passes through the enclosed object, and if it finds arrays of objects, it flattens them into one object. To get the object from the previous example, you need the following code:
{
"{{#flatten}}": {
"no-matter-what-name-is": {
"{{#each: [
{\"name\": \"Washington Capitals\"},
{\"name\": \"Vegas Golden Knights\"},
{\"name\": \"Pittsburg Penguins\"}] as team }}": {
"{{ 'Team ' + (index + 1) }}": "{{item}}"
}
},
"Background": "nhl1-bg.jpg",
"Heading": "2019 Stanley Cup® Championship"
}
}
Important:
- Flattening occurs only at the highest level. If there are other arrays in objects of a command like this, they will remain "as is". If you need to flatten them, add another
#flatten
. - The property names in array elements must be different, otherwise, the last element overwrites previous elements. In other words, objects like
[{name: 'John'}, {name: 'Paul'}]
cannot be correctly flattened, but[name1: 'John', name2: 'Paul']
can. - Array indexes start from
0
. Therefore, if you want to start the first element from1
, do not forget to increment the value manually.
Concatenation of arrays (#concat)
Sometimes you have to join several arrays. Of course, you may write the appropriate code inside the {{}}
expression, but it may make the config too clunky and difficult to read.
Instead, you may use the #concat
command as follows:
{
"{{#concat}}": [[1,2,3], [4,5,6]]
}
It is an equivalent of [1,2,3,4,5,6]
.
It is also possible to use it to append a single item.
{
"{{#concat}}": [[1,2,3], 100]
}
It will result in [1,2,3,100]
.
Although, in practice, you need to work with unified data types, the syntax does not prevent you from mixing different types.
{
"{{#concat}}": [
[1, 2, 3],
4,
"foo",
{"a": 1, "b": "bar"}
]
}
It will result in [1, 2, 3, 4, "foo", {"a": 1, "b": "bar"}]
.
Nested arrays are not flattened.
{
"{{#concat}}": [
[[1, 2]],
[[3,4], [5,6]]
]
}
It will result in [[1,2],[3,4],[5,6]]
.
Merge objects (#merge)
You may want to extend an existing JSON with additional properties or override them if they already exist. In other words, do a similar thing to JavaScript Object.assign()
(or its more advanced equivalents from lodash or other libraries). In this case, you may use the #merge
operator.
To do so, you should pass the array, where the first element is an object you want to extend and other elements are the properties you want to add.
{
"{{#merge}}": [
{
"name": "John",
"company": "FooBar, Inc."
},
{
"age": 35
},
{
"name": "John Silver",
"pictures": ["1.jpg", "2.jpg"]
}
]
}
It will result in:
{
"name": "John Silver", // note, we have overwritten the name
"company": "FooBar, Inc.",
"age": 35,
"pictures": ["1.jpg", "2.jpg"]
}
Note
It does not work with arrays (use #concat
instead) and literals like strings, numbers, etc.
The Context Declaration (#with as)
Sometimes it may happen that you have a number of nodes using the same expression to get values like
vars.teams.filter((team) => $['teams']._.some((sel) => team.name === sel.title))
- getting an object with the same name as the selected option. Constant copying of this expression is redundant.
This problem can be solved by using the #with
operator. It assigns an alias to the specified expression, puts it in the context and automatically reevaluates it when any changes occur. This alias is available in all child elements. In other words, it performs almost the same as #each
performs for the value specified after as
but only for one particular value.
For example:
"{{#with vars.layouts.find((layout)=>layout.id === root.$['layouts']._.id) as curLayout}}": {
"name": "{{curLayout.name}}",
"teamLimit": "{{curLayout.teams}}"
}
If you omit as
, then value
will be put it in the context.
Escaping (#escape)
Double curly brackets are unlikely to appear in the strings you want to use as values of your config. That's why we use it to mark the dynamically evaluated code.
However, under certain circumstances, you may want the config to skip the double curly bracket processing and return the value "as is". For example, in Customer's Canvas, text elements with double curly brackets are the variable fields. If you specify a string like "Hello {{User Name}}"
, it will try to execute the expression User Name
, which will obviously fail.
To prevent this, wrap such expressions into the #escape
operator:
{
"{{#escape}}": "Hello {{User Name}}"
}
It will result in Hello {{User Name}}
.
Links to the functions
Some of the widget properties refer not only to the code that should calculate the value but also to a code that should be called only in case of an event. For example, onClick
of a button or onActivate
of a step.
To mark the function references, use the #function
operator.
{
"type": "button",
"name": "save",
"params": {
"onClick": "{{#function $['editor'].saveProduct()}}"
}
}
Variables
Sometimes, you may need to specify variables in your config. Based on these variables, you can build drop-down lists, say, a list of commands with their schedules. To do this, you need to add the vars
object to the top level of the config and add your data to this object.
{
"vars": {
"teams": [
{"id": 0, "name": "Chicago Bears", "schedule": [], "colors": { "city": "#ff0", "team": "#fff"} },
{"id": 1, "name": "Miami Dolphins", "schedule": [], "colors": { "city": "#f00", "team": "#00f"} }
]
}
}
In the config, you can refer to this variable like this:
{
"value": "{{vars.team[0].name}}"
}
And this:
{
"value": "{{vars.team[root.$['Teams']._.id].name}}"
}
JS in Expression
Debugging
Sometimes the config may become very complicated, and it is not always clear why something does not work as it should.
To troubleshoot problems with the config more efficiently, you can use the @log
and @debug
modifiers in the dynamic expression, like this:
{
"title": "{{@log $['my-option']._.title}}",
"value": "{{@debug vars.data.some(x=>x.title)}}"
}
@log
just wraps the result of the expression into console.log()
. @debug
adds a debugger
keyword (i.e. adds a breakpoint). Of course, to be able to use these modifiers, you should run the editor with the Dev Console opened (F12 in Chrome).
Working with Arrays
Most likely, you will have a complex data structure in variables, from which you need to build an interface. You can use the built-in JavaScript methods for working with arrays:
map
filter
find
reduce
- ...
Thus, the following example illustrates how you can select teams consisting of more than 10 members:
"values": {
"{{#each vars.team.filter(function(t) { return t.members > 10 }) as team}}": {
"title": "{{team.name}}"
}
}
If you are not familiar with the JavaScript functionality to work with arrays, refer to the Array specification.
Arrow functions
When working with arrays, it's often necessary to create functions that check conditions (so-called predicates) or return a changed object on the fly (you can refer to the previous example with the filter).
Modern JavaScript has a more compact way of writing - so-called arrow functions. They allow you to rewrite this example like this:
"values": {
"{{#each vars.team.filter((t) => t.members > 10) as team}}": {
"title": "{{team.name}}"
}
}
However, you can use arrow functions only if you believe that this editor will be used only in modern browsers (that excludes IE11 and possibly some mobile browsers). See the complete list at caniusecom.
String interpolation
Another tool available in modern browsers is the insertion of variable values into text strings. Thus, in the "old style", you would write {{'Item name' + item.name + 'at' + index}}
, and now you can write it as follows:
{{`Item name ${item.name} at ${index} \`}}
It's much easier to read but it is not available in IE.
Sometimes you may face a problem when code editors (or even a browser) can try to evaluate this line immediately and say "You did not pass the index." To avoid this, you can screen the braces:
`Name$\{index+1\}`
In this way, the browser will not know that this is an interpolated string until it gets to the editor.
In the next topic, you can learn how to create a single config and use local variables or attributes of your e-commerce system for Parametrizing configs.