New online demos available.  

Dynamic Configs

The Multistep Editor implements the concept of self-updating configurations when 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 looks 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 to 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, Multistep Editor's 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 this works?

When getting the config, the editor looks for dynamic parts in it. 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 handles 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 an access to some data. Dynamic Configs provide the following entities:

  • $ - the global scope where you can get to the widgets (as an 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 the values of the current widget.
  • product, order, user - objects from the ecommerce driver.

All widgets which 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 with the #flatten operator.

Flattening arrays in objects (#flatten)

Say, we want to create an object of such a 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 need not just an object. This object should 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 such a command, they will remain "as is". If you need to flatten them, put 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 [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 [1, 2, 3, 4, "foo", {"a": 1, "b": "bar"}].

Nested arrays are not flattened.

{
    "{{#concat}}": [
        [[1, 2]],
        [[3,4], [5,6]]
    ]
}

It will result [[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 smarter equivalents from lodash or other libraries). In this case, you may use the #merge operator.

To do it, 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:

{
    "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 yo omit as, then value will be put it in the context.

Escaping (#escape)

Double curly brackets is 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 Hello {{User Name}}.

Some of the widget properties refer not just the code which should calculate the value, but a code which 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 really 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 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).

A 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 as follows:

{{`Item name ${item.name} at ${index} \`}}

It's much easier to read but also 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 will get into the editor.