Back to Website
Show / Hide Table of Contents

Dynamic expressions

  • Last updated on December 29, 2023
  • •
  • 13-14 minutes to read

To extend workflow features, you can use JavaScript code in them. This code is called dynamic expressions. They allow you to manage workflows depending on the condition of widgets. For example, when a customer clicks a checkbox, the next step becomes available. Customers select something in a drop-down list, and a design will be changed. JS code also allows customers to see the personalization result and approve it.

Note

In a workflow file, JS code is enclosed in double curly braces {{...}}.

Let's consider how dynamic expressions work.

Concept

As you read in the Creating and editing workflows article, workflows are defined in the JSON format. This format doesn't allow you to create a self-updating workflow. Nothing will happen, when you select an option or click a button. By adding some JS code to a workflow file, you create interactions between workflow elements, vars, widgets, and so on. For example, when you click an Add image button, an upload dialog box appears, and once an image is selected and uploaded, it is placed in a design.

It works because the system reads a workflow file and tracks all the changes. Although JSON elements aren't changed, dynamic expressions return the result of a function instead. This will happen every time when the customer takes an action.

Note! You can only embed a dynamic expression in the following elements:

  • Params of a widget
  • Variables
  • Name of a step

To learn more about the anatomy of each of these elements, read the Widgets and the Structure articles.

Let's look at examples of dynamic expressions.

In this code, you can see vars and a widget with a dynamic expression. In the expression, you can see a variable var1 with its string Hello and a string world!.

{
    "vars": {
        "var1": "Hello"
    },
    "widgets": [
        {
            ...
            "params": {
                "param1": "{{vars.var1 + ' world!'}}"
            }
        }
    ]    
}

The system will resolve this expression to a string and the resulting code will appear as follows:

{
    "vars": {
        "var1": "Hello"
    },
    "widgets": [
        {
            ...
            "params": {
                "param1": "Hello world"
            }
        }
    ]    
}

The same works in key: value syntax. In this example, a variable var1 will be replaced with its value MyParamName in params.

{
    "vars": {
        "var1": "MyParamName"
    },
    "widgets": [
        {
            ...
            "params": {
                "{{vars.var1}}": 123
            }
        }
    ]    
}

The result is the following.

{
    "vars": {
        "var1": "MyParamName"
    },
    "widgets": [
        {
            ...
            "params": {
                "MyParamName": 123
            }
        }
    ]    
}

You can manipulate arrays and objects in dynamic expressions. In the following example, we place an object and an array in params.

{
    "vars": {
        "var1": {
            "a": "b"
        }
    },
    "widgets": [
        {
            ...
            "params": {
                "param1": "{{vars.var1}}",
                "param2": "{{[vars.var1, 123]}}"
            }
        }
    ]    
}

This is the result of the expression resolutiton.

{
    "vars": {
        "var1": {
            "a": "b"
        }
    },
    "widgets": [
        {
            ...
            "params": {
                "param1": { "a": "b" },
                "param2": [{ "a": "b" }, 123]
            }
        }
    ]    
}

Syntax

You've already seen that JS code is placed in double curly brackets.

{{some JS code}}

You can embed JS code inside.

{{ 1 + 1 }}

This expression can also contain a reference to a variable.

{{ vars.design }}

Referring to widgets

Let's consider how to refer to widgets and get their values.

This example illustrates two widgets: widget1 and widget2. The widget2 contains a dynamic expression. This expression refers to the widget1 and the value of its params1.

  • The $ symbol refers to a scope. It allows you to refer to a widget defined in the config. To do so, use the name of a widget. For example: {{ $['my-widget-name'] }}.

  • The .param1 allows you to refer to the parameter in a widget. The . symbol means that you're already in a widget you need and denotes its parameters.

{
    "widgets": [
        {
            "name": "widget1",
            "title": "My widget",
            "type": "...",
            "params": {
                "param1": 2
            }
        },
        {
            "name": "widget2",
            "type": "...",
            "params": {
                "text": "{{$['widget1'].param1}}"  
               
            }
        }
    ]        
}

Referring to a widget with a selected value

Let's consider another case. The main conceptual feature of some widgets are values defining these widgets. For example, the main value of the input-text widget is the text, which a customer types.

This example illustrates two widgets: my-option and my-widget. The latter widget refers to my-option to get the value.

To get a selected value, use the .selected syntax, which can be replaced by the ._ alias.

{
  "widgets": [
    {
      "name": "my-options",
      "type": "option",
      "title": "Select the option",
      "params": {
        "type": "radio",
        "title": "This is your options:",
        "values": [
          {
            "title": "Sky blue"
          },
          {
            "title": "Scarlet"
          },
          {
            "title": "Beige"
          }
        ]
      }
    },
    {
      "name": "my-widget",
      "type": "...",
      "params": {
        "text": "{{$['my-options'].selected}}"  
      }
    }
  ]      
}

This is how an equivalent definition with ._ will look.

{
  ...
  "widgets": [
    {
      "name": "my-widget",
      "type": "...",
      "params": {
        "text": "{{$['my-options']._}}"  
      }
    }
  ]    
}

Referring to a widget itself

Sometimes you need to refer to another parameter of the same widget. To avoid cycle dependency, you can use the self alias.

In this example, you can see a widget with two params. The param1 has a boolean value, and the param2 gets the value of param1.

{
    "widgets": [
        {
            "name": "widget1",
            "title": "My widget",
            "type": "...",
            "params": {
                "param1": true,
                "param2": "{{self.param1}}"
            }
        }
    ]        
}

Referring to variables

As you have already learned, you can refer to variables defined in a separate block in a workflow file. Read the Structure article to learn about defining variables.

{
    "vars": {
        "name": "my-string"
    },

    "widgets": [
        {
            "name": "widget-name",
            "type": "...",
            "params": {
              "param1": "{{vars.name}}"
            }
        }
    ]
}

You can also refer to variables containing arrays or compound objects.

Variables are not only in widget parameters. For example, you can manage the name of a step.

{
  "vars": {
    "condition": true,
    "ifTrue": "True",
    "ifFalse": "False"
  },
  "steps": [
    {
      "name": "{{ vars.condition ? vars.ifTrue : vars.ifFalse}}",
      "mainPanel": {...}
      }
    },
    {
      "name": "{{ !vars.condition ? vars.ifTrue : vars.ifFalse}}",
      "mainPanel": {...}
    }
  ]
}

Referring to attributes

As in the case of variables, you can refer to attributes by using the product object. The following example is taken from a real workflow file.

{
    "attributes": [
        {
            "name": "Design",
            "type": "single-asset",
            "assetType": "design"
        }
    ],
    "vars": {
        "design": "{{product.attributes?.find(x=>x.name==='Design')?.value}}",
    }
}

Expressions

Dynamic expressions can also use some JavaScript expressions through the {{#...}} syntax. Let's see some examples.

Conditions (#if/#elseif/#else)

The #if/#elseif/#else expression adds a conditional logic to your workflow. It looks like this:

{
    "{{#if somevalue}}": "value-1",
    "{{#elseif anothervalue}}": "value-2",
    "{{#else}}": "value-3"
}

Depending on the condition passed to the expression, it will return different result. In the example above, if somevalue is true, the entire object containing this expression will be replaced by value-1.

If somevalue is false, but anothervalue is true, then the object will be resolved to value-2. Finally, if both the conditions are false, the result will be value-3.

You may use any JavaScript instead of somevalue or anothervalue, just like in regular dynamic expressions. In the following example, the result will depend on the var1 value.

{
    "vars": {
        "var1": ...
    },
    "widgets": [
        {
            ...,
            "params": {
                "param1": {
                    "{{#if vars.var1 > 0}}": "var1 is positive",
                    "{{#elseif vars.var1 = 0}}": "var1 is zero",
                    "{{#else}}": "var1 is negative"
                }
            }
        }
    ]
}

Let's see the result. If vars.var1 is 10, the first condition will be true and will return the var1 is positive string.

{
    "vars": {
        "var1": 10
    },
    "widgets": [
        {
            ...,
            "params": {
                "param1": "var1 is positive"
            }
        }
    ]
}

If vars.var1 is 0, the second condition is met:

{
    "vars": {
        "var1": 0
    },
    "widgets": [
        {
            ...,
            "params": {
                "param1": "var1 is zero"
            }
        }
    ]
}

If vars.var1 is -10, the value will be taken from the third expression:

{
    "vars": {
        "var1": -10
    },
    "widgets": [
        {
            ...,
            "params": {
                "param1": "var1 is negative"
            }
        }
    ]
}

It is possible to omit {{#elseif}} or use many of them.

You can also omit {{#else}}. However, when the condition is false, the result will be an undefined value, which is almost always undesired.

Each as

The each expression returns an array formed from element values.

"names": {
   "{{ #each [{first: 'John', last: 'Lennon'}, {first: 'Paul', last: 'McCartney'} as person ] }}": "{{ person.first + ' ' + person.last }}"
}

The result will be as follows:

"names": [ "John Lennon", "Paul McCartney" ]

Let's see another example.

"names": {
   "{{ #each [{id: 0, name: 'Chicago Bears'}, {id: 1, name: 'Washington Redskins'}] as item }}": {
         "id": "{{ team.id }}",
         "title": "{{ team.name }}",
         "price": 0
    }
}

This expression will be resolved to the names array.

"names": [
  {
     id: 0,
     title: "Chicago Bears",
     price: 0
  },
  {
     id: 1,
     title: "Washington Redskins",
     price: 0
  }
]

Let's see how this expression can be used in a real workflow file. In the following example, the slider widget displays proof images and is used at the Approval step.

{
    "name": "preview",
    "type": "slider",
    "params": {
        "style": {
            "--au-widget-background": "#eee",
            "--au-widget-padding": "8px"
        },
        "direction": "tile",
        "rows": 1,
        "columns": "{{Math.min(2,$['editor'].proofImageUrls.length)}}",
        "containerColor": "#eee",
        "images": {
            "{{#each $['editor'].proofImageUrls.map(s=>s[0]) as imageUrl }}": {
                "url": "{{imageUrl}}"
            }
        }
    }
}

In this example, slider shows the personalization result. It doesn't know how many proof images have been created.

"images": {
    "{{#each $['editor'].proofImageUrls.map(s=>s[0]) as imageUrl }}": {
        "url": "{{imageUrl}}"
    }
}

Another example illustrates how you can populate an array of option widget values with values defined in vars.

{
    "vars": {
        "list": {
             ['Apples', 'Orange', 'Pear']
        }
    },
    "widgets": [
        {
            "type": "option",
            "name": "my-options",
            "params": {
                "type": "list",
                "title": "Select an item",
                "values": {
                    "{{#each vars.list }}": {
                        "title": "{{`value=${item}, index=${index+1}`}}", 
                        "index": "{{index}}",
                        "value": "{{item}}"
                    }
                }
            }
        }
    ]
}

With as

This expression allows you to create a local variable that will be available in the following piece of code.

"{{#with vars.layouts.find((layout)=>layout.id === $['layouts']._.id) as curLayout}}": {
   "name": "{{curLayout.name}}",
   "teamLimit": "{{curLayout.teams}}"
}

When any changes occur, such a variable will be automatically reevaluated. In the following example from a real workflow file, the productSize will be changed as soon as you change the browser width or height.

{
    "vars": {"sizes": ["Small", "Medium"]},
    ...
    "widgets": [
        ...
        {
            ...
            "params": {
                "...": {
                    "{{ #with $['height-value']._ <= 387 || $['width-value']._ <= 387 ? 'Small' : $['height-value']._ <= 1000 && $['width-value']._ <= 1200 ? 'Medium' : 'Large' as productSize }}": {
                        "title": "{{ productSize }}",
                        "isSupportedSize": "{{ vars.sizes.some(x => x == productSize) }}"
                    }
                }
            }
        }
    ]
}

You can also use this expression in nested objects.

"props": {
    "{{#with 1 + 1 as myValue}}": {
        "value2": {
            "{{#with 2 + 2 as myValue}}": "{{myValue}}"
        },
        "value3": "{{myValue}}"
    }
}

Escape

The #escape expression allows you to apply escaping and use double curly brackets as is without the resolution.

For example, you must use it in the Design Editor for VPD products with interpolated strings. When you enclose such strings in double curly brackets, the system will try to resolve {{UserName}} as a dynamic expression when it should not.

{
    "Header": "Hello {{#escape UserName}}"
}

The result will be a string.

{
    "Header": "Hello {{UserName}}"
}

This expression is constantly used in products where you add variable text. Once a product is designed, these text elements will be located, and a list of their values will populate in a table. Then, a single product will be rendered for every table row.

The example of VDP product with text fields.

This is how you can define a Variable Text button in the editor widget to implement this scenario.

{
    "translationKey": "Variable Text",
    "translationKeyTitle": "Add a variable text to the canvas",
    "iconClass": "fa fa-check-square-o",
    "action": "text",
    "itemConfig": {
        "name": "Custom Text",
        "text": "{{#escape Custom Text}}",
        "isVariable": true,
        ...
    }
}

Merge

This expression merges objects.

{
    "{{#merge}}": [
        {
            "name": "John",
            "company": "FooBar, Inc."
        },
        {
            "age": 35
        },
        {
            "name": "John Silver",
            "pictures": ["1.jpg", "2.jpg"]
        }
    ]
}

This expression will result in the following object.

{
    "name": "John Silver", // note, we have overwritten the name
    "company": "FooBar, Inc.",
    "age": 35,
    "pictures": ["1.jpg", "2.jpg"]
}

Concat

The #concat expression concatenates several arrays into one.

In this example, there is one array with two arrays inside. The concat expression unites two arrays into one.

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

This results in [1, 2, 3, 4, 5, 6].

You may have an array that contains a nested array and an item inside.

{
    "{{#concat}}": [[1,2,3], 100]
}

In this case, #concat returns the following array: [1,2,3,100].

Now, let's look at an example from a real workflow file. This is the part of the editor widget defining toolbox buttons.

"Toolbox": {
    "buttons": {
        "{{#concat}}": [
            [
                {
                    "translationKey": "Toolbox.TEXT",
                    "translationKeyTitle": "Toolbox.TITLE_ADD_TEXT",
                    "iconClass": "cc-icon-add-text",
                    "buttons": [
                        "Text",
                        "BoundedText"
                    ]
                },
                {
                    "translationKey": "Toolbox.SHAPE",
                    "translationKeyTitle": "Toolbox.TITLE_ADD_SHAPE",
                    "iconClass": "cc-icon-add-shape",
                    "buttons": [
                        "Line",
                        "Rectangle",
                        "Ellipse"
                    ]
                },
                "QrCode",
                {
                    "action": "Image",
                    "translationKeyTitle": "Upload an Image",
                    "iconClass": "cc-icon-uploadable",
                    "tabs": [
                          "My files"
                    ]
                }
            ]
        ]
    }
}

Flatten

The #flatten expression transforms an array to object properties. Why might you need this?

Let's imagine that you have a list of baseball teams:

{
    "vars": {
        "teams": [ "Los Angeles Dodgers", "New York Yankees", "Houston Astros"]
    }
}

Now, let's assume that you need to prepare the following structure, for example, to send it to the server using the ajax widget.

{
    "Team 1": {
        "name": "Los Angeles Dodgers"
    },
    "Team 2": {
        "name": "New York Yankees"
    },
    "Team 3": {
        "name": "Houston Astros"
    },
    "Heading": "2023 MBL",
    "Background": "green-field-01.jpg"
}

You may try to implement this through a combination of #merge, #each or use complicated logic that would involve a lot of #if statements. However, it would be quite challenging.

The #flatten comes to rescue. You can write something like this:

{
    "{{#flatten}}": {
        "_": {
            "{{#each vars.teams as team}}": {
                "{{`Team ${index + 1}`}}": {
                    "name": "{{team}}"
                }
            }
        },
        "Heading": "2023 MBL",
        "Background": "green-field-01.jpg"
    }
}

It will yield the same result as described above.

The #flatten scans the object in the right side of the expression. If it meets a regular property that holds a string, like Heading or Background in our example, it leaves them in the resulting object as is. However, if it meets a property holding an array of objects, it will pull each property of each element and add it to the resulting object.

The name of a property containing such arrays is ignored, which why you may write whatever you want, for example, _ or any other string.

Any non-objects in such an array are ignored.

The #flatten processes only one level. If it meets an object that contains arrays, they are copied to the result as is.

Function

You can implement handling the onClick, onChange, onActivate, and other events in your workflow file. They will execute code only when an event happens. For example, you add code, which is executed after clicking a button.

The syntax is the following:

{
    "onClick": "{{#function <some js expression> }}"
}

It's important that <some js expression> returns a value. For example, this expression is true.

{
    "onClick": "{{#function $['editor'].getProofImages() }}"
}

The following expression returns an error because console.log is a statement and doesn't return a value.

{
    "onClick": "{{#function console.log('Hello world') }}"
}

If you want to implement several actions in a handler, you can write them as an array of functions.

{
    "onClick": [
        "{{#function <some js expression 1> }}",
        "{{#function <some js expression 2> }}"
    ]
}

The system will execute them one by one. If a function is asynchronous, the next function will run after the previous function is executed.

Debugging

When you are writing a workflow file, something can go wrong. You can use the following keywords for troubleshooting.

@log

You can add @log to the beginning of a dynamic expression to output its value:

The usage of @log in a browser console.

{
    "name": "my-checkbox",
    "type": "checkbox",
    "params": {
        "prompt": "This is my checkbox.",
        "value": false
    }
},
{
    "name": "my-button",
    "type": "button",
    "params": {
        "text": "Click me",
        "enabled": "{{ @log $['my-checkbox']._}}"
    }
}

Every time the system resolves this expression, its result appears in the browser console. To open this console, press F12 and click the Console tab.

@debug

The @debug keyword works like a breakpoint for a dynamic expression. You can add it to the beginning of a dynamic expression. Every time the system implements a dynamic expression, a browser will stop at this Java Script code. You can debug the code in the debugger window.

The usage of @debug keyword in a browser console.

{
    "name": "my-checkbox",
    "type": "checkbox",
    "params": {
        "prompt": "This is my checkbox.",
        "value": false
    }
},
{
    "name": "my-button",
    "type": "button",
    "params": {
        "text": "Click me",
        "enabled": "{{ @debug $['my-checkbox']._}}"
    }
}

Now you can learn how to use dynamic expressions in widgets. To do so, read the Widgets articles.

Was this page helpful?
Thanks for your feedback!
Back to top Copyright © 2001–2024 Aurigma, Inc. All rights reserved.
Loading...
    Thank for your vote
    Your opinion is important to us. To provide details, send feedback.
    Send feedback