TrophyCustomer's Canvas is honored with a 2020 InterTech Technology Award! Learn more 

Dynamic Configs

The multi-step editor 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
  • Cycles
  • 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.
  • vars - config variables.
  • self - refer to the values of the current widget.
  • product, order, user - objects from the e-commerce driver.

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.

Cycle (#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:

  1. 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.
  2. 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.
  3. Array indexes start from 0. Therefore, if you want to start the first element from 1, 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}}.

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.