Form Evaluations

This section describes how the Form.io Renderer evaluates javascript code.

JavaScript Evaluation

Within the renderer and builder code, it is possible write custom snippets of JavaScript to perform custom actions that would otherwise be difficult to configure. These snippets of JavaScript can be seen within the Form Builder interface under sections such as Custom Default Value, Calculated Values, Logic, as well as many others. The typical interface for such JavaScript snippets is shown within a special javascript editor within the form builder with a section above that showing the variables available within the execution context, such as the following image shows.

Evaluation Context

For every javascript evaluation that occurs, there are a number of variables that are presented within the execution sandbox. These variables are commonly referred to as the evalContext variables. Some of the evaluations provide their own evalContext, which will be documented below, but the following eval variables that are common across all evaluation types.

VariableDescriptionExample

form

The complete Form JSON schema of the form.

{

"_id": "....",

"path": "user",

"name": "user",

"display": "form",

"components": [

{

... } ] }

submission

The complete Submission JSON of the current submission for the rendered form.

{ "data": { "first": "Joe", "last": "Smith" }, "metadata": {} }

component

The current Component JSON schema.

{ "type": "textfield", "label": "Name", "key": "name" }

value

The current component value.

show = value === 5;

instance

This points to the component instance. This is helpful if you wish to gain access to the actual component instance object to perform special functions and execute certain methods of the component. Go to https://github.com/formio/formio.js/blob/master/src/components/_classes/component/Component.js to see what methods you can access.

valid = instance.checkValidity()

self

Alias of "instance"

instance.root

This always points to the form instance that contains the component. This is helpful if you wish to reference other components using the following code.

instance.root.getComponent('email') = 'joe@example.com';

options

The Form Renderer Options passed to the renderer.

{ "i18n": { "language": "es", "es": { ... } } }

data

The root data context for the renderer. This will always point to the full data object from "submission.data"

{ "first": "Joe", "last": "Smith", "children": [ { "first": "...", "last": "..." } ] }

row

The "row" variable is a special "context" data object that points to the current data "context" of the component. This changes based on what component we are referring to. For example, if you are within a DataGrid component (which is an array of objects), the "row" will point to the current row object.

Let's suppose you have a DataGrid called Children, and you wish to write javascript to validate the birthday component within the children DataGrid. You would be able to use "row" to point to the "current" row's birthday field like the following. row.birthday

rowIndex

This is the index for the current row you are on. For DataGrid and EditGrid components, this is a number where 0 means we are on the first row, 1 means we are on the second row, etc.

0 - first row

1 - second row

...

t

A function, which is used to translate certain strings using the Translation system.

value = t('First Name');

_

An instance of Lodash which can be used to simplify certain operations within your javascript code.

value = _.get(data, 'a.b')

utils

An instance of the Form Utilities.

utils.eachComponent(component.components, function(component) { ... });

util

Alias of "utils"

user

The currently authenticated User object.

value = user.data.email;

token

The current JWT token for the authenticated user.

moment

An instance of the Moment.js library.

valid = moment.diff(value, '12/5/2021') > 0;

config

The current Public Configuration config that has been added to the form json.

Custom Evaluation Context Variables

In addition to having the standard variables as shown above, it is possible to also introduce your own evaluation context variables that can be used within the evaluations. This is very helpful in case you have pre-defined methods for validations, etc. that you would like to expose to all of the evaluations. This can be achieved using either the Form Module found in your Project Settings, or through the embedding of the form.

Form Module Example

Within your Project Settings, click on Settings > Custom JS and CSS. Within this section, you will see a section called Form Module which is used to write a snippet of JSON that is able to dynamically configure the form as it is being embedded within an application. You can introduce a new context variable as follows.

{
  options: {
    form: {
      evalContext: {
        validatePhone: function(input) {
          return input.match(/^[2-9]\d{2}-\d{3}-\d{4}$/);
        }
      }
    }
  }
}

With this example, there would now be a new method available in the evaluation context called "validatePhone" and could be used as follows within a Custom Validation block.

valid = validatePhone(input) ? true : 'Phone number is invalid';

Form Embedding Example

You can also set custom evaluation context variables when you embed the form. The following shows an example of how this could be done.

Formio.createForm(document.getElementById('formio'), 'https://examples.form.io/example', {
  evalContext: {
    validatePhone: function(input) {
      return input.match(/^[2-9]\d{2}-\d{3}-\d{4}$/);
    }
  }
})

This can then be used in the same way as described above.

Now that we have an understanding of evaluation contexts, let's discuss all the different places where javascript evaluations can be performed. There are many sections that allow for JavaScript evaluation. These sections are described as follows.

Custom Default Value

This provides a way to set the custom default value of the component you are currently configuring. The default value is the value that is used at the initialization of the component and provides you an opportunity to set the initial value of the component, but also provides a good point to place any initialization code you may wish to add to the component.

Additional Evaluation Context

For custom default values, there are a few additional evaluation context variables that are used.

VariableDescriptionExample

value

The value to set as the custom default value.

value = 5;

Example 1: Set custom default value to the combination of other fields.

value = data.firstName + " " + data.lastName;

Example 2: Listen for change events of this component and set the value of another component

instance.on('change', function(event) {
  if (
    event && 
    event.changed && 
    event.changed.component && 
    event.changed.component.key === component.key) 
  {
    instance.root.getComponent('email') = instance.getValue() + '@example.com';
  }
});

Calculated Value

The calculated value snippet allows you to write custom pieces of javascript that set the value of a component. The value is set by setting the variable "value" within the snippet of javascript.

Additional Evaluation Context

For calculated values, there are a few additional evaluation context variables that are used.

VariableDescriptionExample

value

The value to set as the calculated value.

value = 5;

Example 1: Perform a Total Amount calculation on values in a data grid

Assuming that there is a Data Grid component called "Scores" that contains a component within the data grid called "Score", you could have a component outside of the data grid, called "Total" with the following calculated value.

value = data.scores.reduce(function(total, row) {
  return total + row.score;
}, 0);

Example 2: Conditionally calculate a value

You can also choose to set the value within an if statement, and this would only set the value under certain conditions. For example, this could be used to force a "maximum" value.

if (instance.getValue() > 10) {
  value = 10;
}

Custom Validation

Custom Validations allow you to write a snippet of JavaScript that decides how the component should be validated as well as what error to show when the evaluation is determined to be invalid. This can be achieved using the following additional context variables exposed to the custom validation section.

Additional Evaluation Context

For calculated values, there are a few additional evaluation context variables that are used.

VariableDescriptionExample

input

The value that has been input into the component that is being compared for evaluation.

valid = input === 5;

valid

A special variable that determines if the component is valid. If the value is set to "true", then the component is valid. Otherwise you would set the value of "valid" to the string you would like to show the user when it is invalid.

valid = input === 5 ? true : 'The value must be 5!';

Example 1: Validate that this "validate password" field matches the "password" field.

valid = input === data.password ? true : 'Passwords must match!';

Advanced Conditions

Advanced conditions allow you to write a snippet of Javascript that determines the visibility/validation condition for the component. When the value of a conditional is set to false, the component effectively becomes "inactive" which means it is both not included visibly in the form, but also is not evaluated for validity. This is commonly used to present different sections of the form based on the answers provided by other fields.

Additional Evaluation Context

For advanced conditions, there are a few additional evaluation context variables that are used.

VariableDescriptionExample

show

Determines if this component is visible or not. If show is equal to "false", then the component not only becomes invisible, but also is no longer evaluated for validity. For example, if the field is required, but is conditionally not visible, then it no longer is required.

show = value === 'Testing';

value

The current value of the component that is being compared for evaluation.

show = value === 5;

Logic: JavaScript Trigger

Within the Logic tab, there is an ability to add Logic to your forms to perform different operations such as hiding the component, making it required, etc under certain conditions. The triggers for logic determine how the current logic section is triggered.

Additional Evaluation Context

For calculated values, there are a few additional evaluation context variables that are used.

VariableDescriptionExample

result

Determines if the logic section should be triggered. If result is set to "true" it will be triggered, if it is set to "false", then it will not

result = data.email === 'admin@form.io';

Example 1: Trigger the logic if the average grades are less than 70

var total = data.grades.reduce((total, grade) => total + grade, 0);
var average = total / data.grades.length;
result = average < 70.0;

Logic: Custom Actions

Within the Logic, after a logic section has been triggered, it will now perform the "action" of that logic. This is the "do something" part of the logic section where it performs an operation. Within this section, it is possible to write your own javascript to perform the action you would like to perform within the action.

Example 1: Set the component value when the value executes.

value = data.score1 + data.score2;

JSON Logic

Within every evaluation type, there is also the ability to configure the evaluations using JSON schemas. This is helpful if you wish to perform complex evaluations without the requirement of executing the JavaScript "eval" necessary to perform the javascript evaluations of the scripts shown above.

There are other options for not using "eval" such as using the protected eval plugin described below, but JSON Logic also serves as a good strategy for such protections.

The JSON Logic system uses the JSON Logic Library to perform the evaluations needed for each of the evaluation sections. Below are some examples of the different evaluation types on how this system can be used to create complex evaluations without using any JavaScript.

Custom Default Value

Example 1: Concatenate the value of two different components string values together.

{"cat": [{"var": "data.firstName"}, " ", {"var": "data.lastName"}]}

Calculated Value

Example 1: Sum together a multiple value number component

{"_sum": {var: "data.multinumber"}}

Custom Validation

For custom validations, you need to always use the "if" parameter within JSON Logic. The first argument to the "if" statement is the "true" case, and the second should be the error that you show when the value is not set. The following shows examples of how this is used.

Example 1: Validate if a string is equal to a value.

{
  "if": [
    {
      "===": [
        {
          "var": "input"
        },
        "Bob"
      ]
    },
    true,
    "Your name must be 'Bob'!"
  ]
}

Example 1: Validate if an email is from a certain domain

{
  "if": [
    {
      "in": [
        {
          "var": "input"
        },
        "form.io"
      ]
    },
    true,
    "The email must be from 'form.io'!"
  ]
}

JavaScript Evaluator

While the Form.io platform offers a large amount of flexibility with the evaluations and executions of scripts within the renderer, it also includes even more flexibility and extensibility through the use and modification of the Evaluator within the renderer.

The Evaluator is a static class object where all javascript executions pass through to be evaluated. The code for the basic Evaluator class can be found at the following code.

This class is used to perform any executions and 'eval' processes within the renderer from template rendering to the execution of javascript snippets within the renderer. The instance can also be referenced within your application by simply accessing the following property on the global Formio object.

Formio.Evaluator

Using this instance, you can easily configure how the Evaluator behaves such as the following describes.

Configuring the Evaluator

There are many ways that you can use the instance of the Evaluator to modify how it behaves by default. Some of these ways are described below:

Turn off evaluations

The first thing you may wish to do is completely disable all evaluations from occurring. You would want to do this if you are running the renderer within a tight security environment where you may not trust the form builders who created the form, and worry about the execution of malicious JavaScript within your application. This can be done by setting the noeval property to true like so.

Formio.Evaluator.noeval = true;
Formio.createForm(document.getElementById('formio'), ...);

Changing the template evaluation settings

The evaluator is also used to evaluate all templates and perform interpolations, such as the following.

Your first name: {{ data.firstName }}

This string may be contained within an HTML Element component within the form, but since it contains a token, it will be interpolated using the evalContext as described in the section above. It is possible to change the format of all of these templates to use a different syntax. For example, if you wish to change it to the following.

Your first name: <% data.firstName %>

you could use the templateSettings property of the Evaluator to accomplish this goal.

Formio.Evaluator.templateSettings = {
  evaluate: /\{%([\s\S]+?)%\}/g,
  interpolate: /\<\%([\s\S]+?)\%\>/g,
  escape: /\{\{\{([\s\S]+?)\}\}\}/g
};

These settings are described as follows.

  • evaluate: This is the template for string "evaluations" where it executes the javascript within. This requires that you have evaluations turned on by ensuring the "noeval" flag is not set or set to false.

  • interpolate: This is where you can provide "interpolated" replacement values based on the values provided by the evalContext. For example {{ data.email }} will replace that token with the value from "data" with the key of "email"

  • escape: This configuration tells the interpolation how to find "escaped" tokens. For example, if you wish to actually display {{ data.email }} and do not wish for it to be interpolated, then you will use the following string. \{\{ data.email \}\}

Overriding the Evaluator

In addition to providing configurations for the Evaluator, it is also possible to perform overrides to inject your own custom code into how evaluations are performed. This can be done by injecting your own methods into the Evaluator that "inject" your own code into a method.

As a quick example of something that could be done, let's suppose you wish to console.log all evaluations that were being performed within the renderer, the following code could be used to "inject" this into the renderer evaluation execution.

var evaluate = Formio.Evaluator.evaluate;
Formio.Evaluator.evaluate = function(func, args) {
  console.log(func, ...args);
  return evaluate(func, args);
}

This method shows how you can save off the original function as a variable, then override the method, and then use that original saved function as the function you return with the evaluation. This allows you to inject your own console.log into the process so that you can understand all evaluations that are being executed.

The following methods can be overridden:

  • evaluator(func, ...params) - Returns a method of evaluation. By default, this returns a new function that will be evaluated at a later time.

  • template(template, hash) - Converts a string into a template function that will be executed later.

  • interpolate(rawTemplate, data, _options) - Accepts a string template, along with data context variables, and returns the interpolated result string.

  • evaluate(func, args) - Executes the evaluator function (above) with the arguments provided.

Example: Override Evaluator to work with Node.js VM2

A very good example of how to override the Evaluator can actually be found within our Open Source server code which overrides the evaluator to perform all javascript evaluations within a VM on the server to protect against malicious code. This code in complete form can be found @ https://github.com/formio/formio/blob/master/src/util/util.js#L56

Here is a snippet that shows how the evaluator can integrate with VM2.

const {VM} = require('vm2');
const vm = new VM({
  timeout: 250,
  sandbox: {
    result: null,
  },
  fixAsync: true
});

Formio.Evaluator.noeval = true;
Formio.Evaluator.evaluator = function(func, args) {
  return function() {
    let result = null;
    /* eslint-disable no-empty */
    try {
      vm.freeze(args, 'args');

      result = vm.run(`result = (function({${_.keys(args).join(',')}}) {${func}})(args);`);
    }
    catch (err) {}
    /* eslint-enable no-empty */
    return result;
  };
};

This code does a couple of things.

  1. It turns off any normal evaluations by setting "noeval" to true.

  2. It creates a new VM to perform any javascript executions within.

  3. It overrides the "evaluator" method to execute the script within a VM and return the result. It also ensures that the ONLY variables that the script has access to within the VM are those provided by the evalContext.

Custom Evaluators

In addition to overriding the base Evaluator, the renderer also enables the ability to create your own Evaluator class and register it as the new evaluator for the renderer, this documentation shows you how this can be done.

Building your own custom evaluator

Very similar to overriding the methods described above, it is possible to completely write your own Evaluator class, and then register that Evaluator as part of the renderer. In order to accomplish this, you will need to provide implementation to a few methods that are called from the renderer, which is defined below.

const CustomEvaluator = {
  /**
   * Takes a string function and returns an evaluator that will perform the execution.
   *
   * @param func - This is the string representation of the function that will be executed.
   * @param params - A spread of parameters that are being passed to the function to be executed.
   *
   * @return - This function should return a method that will then be passed to the "evaluate" method of this class. 
   */
  evaluator(func, ...params) {
  },
  
  /**
   * Takes a template string, and then returns a template function that will be executed later with evalContext variables.
   *
   * @param template - A string of the template that needs to be turned into a template function.
   *
   * @return - The template function that will be executed in the future with the evalContext variables.
   */
  template(template) {
  },
  
  /**
   * A method that takes a string, and some data, and then returns the interpolated result of that string.
   *
   * @param rawTemplate - The string version of the template that should be interpolated.
   * @param data - The data that will be passed to the interplation.
   * @param options - Configuration options to use when interplating.
   *
   * @return - A string that has been interpolated. 
   */
  interpolate(rawTemplate, data, options) {
  },
  
  /**
   * Perform an evaluation
   *
   * @param func - The function that was returned by the "evaluator" method.
   * @param args - The function arguments to be passed to the evaluation function.
   *
   * @return - The result of the execution.
   */
  evaluate(func, args) {
  }
};

Once you have implemented your own Evaluator class, you can then register this class using the registerEvaluator method as follows.

Formio.Evaluator.registerEvaluator(CustomEvaluator);

It is also possible to create a Form.io module, and include your custom evaluator within the export of your module as follows.

export default {
    evaluator: CustomEvaluator
}

Then, someone can implement your evaluator by simply using the module as follows.

import { Formio } from 'formiojs';
import EvaluatorModule from 'yourevaluator';
Formio.use(EvaluatorModule);

Protected Evaluator

As a good example of an Open Source module that implements a Custom Evaluator, but also serves a large benefit of providing a "protected" evaluator using JS Interpretor, you can take a look at the library https://github.com/formio/protected-eval

This library can be installed and used as follows.

npm install --save @formio/protected-eval
import ProtectedEval from '@formio/protected-eval';
import { Formio } from 'formiojs';
Formio.use(ProtectedEval);

There are some differences in using this evaluator that need to be understood, so please read more information about this evaluator by going to the following Github repo.

Last updated