blog

How to Configure a Dynamic Form with Calculated Fields

Introduction

SurveyJS allows you to calculate field values based on user input by utilizing data aggregation operations across rows or columns of your calculator form. These arithmetic operations can include calculating totals, averages, minimum and maximum values, as well as counts. In this post, we'll demonstrate how to build and configure a dynamic order form that allows users to order coffee by adding or removing items within a Dynamic Matrix. We'll use predefined matrix totals to calculate overall amounts and utilize custom expression fields to aggregate data within the rows of our dynamic form.

Live Demo

Add a Dynamic Matrix to Your Form

In SurveyJS Creator, drag a Dynamic Matrix to the design surface or simply click the Add Question button and change the question type to Dynamic Matrix using the on-question drop-down menu.

SurveyJS: Add a Dynamic Matrix

Select a newly-created dynamic matrix, and in the Property grid under General assign a unique question name and title and add an optional description (helper text). You can also edit these settings on the design surface using on-question editors.

SurveyJS: Define Question Name and Title

Configure Columns of your Calculator Form

In SurveyJS, a Multi-Select Matrix and a Dynamic Matrix support various data entry options within a cell including a Single-Line input, Long text, Single- and Multi-Select Dropdown Menu, Radio button group, Checkboxes, and more.

The data input type of all matrix cells is defined by the Cell type property. The default cell type is a Dropdown menu. The list of choices for each cell is defined by the Choices property. The on-hover pen icon on each cell activates an individual pop-up where choice options can be set directly for the question. Alternatively, the options can be assigned in the Property grid under Choices.

Coffee Column

To configure different column settings, select a column and navigate to a Property Grid to specify a column type and related settings. Select the first matrix column and change its Cell type to Dropdown. Define a unique column name and a title.

SurveyJS: Configure Dropdown Column

In the Property grid, navigate to the Choices category and create drop-down items using the Choices editor. Each choice has a unique value (its ID) defined in a first column and a user-friendly title in the second column that will be visible to your respondents.

SurveyJS: Configure Dropdown Choices

Enable Unique Values Within a Column

To prevent duplicate responses within the product column and make sure a user always selects different coffee type in each line, navigate to the General tab and enable the Is Unique option for the Coffee column.

Prevent Empty Column Values

To ensure that a user always provide an answer within the product column, select the target column and under General enable the Required option.

SurveyJS: Enable Unique and Required Settings for a Matrix Column

Price Column

In our coffee order table, each coffee product item should have an associated price. To associate a price with the corresponding item, in your real-life application, you may wish to implement a custom function which would return a product price based on the selected product name. In this demo, we will opt for a more simple solution and use a conditional iif (Immediate If) function to return a price based on the selected coffee item.

Select the second matrix column, set its name to "price" and title to "Price". Set the column's Cell type to Expression. Navigate to the Expression property grid category and specify the conditional iif expression. Use the {row.columnName} syntax to reference the selected coffee item within a row.

iif({row.coffee} = 'ristretto' 
   or {row.coffee} = 'macchiato' 
      or {row.coffee} = 'cappuchino', '2.5', 
         iif({row.coffee} = 'flatWhite' or {row.coffee} = 'latte', 3, 
            2))
SurveyJS: Configure Expression Column

Amount Column

Create the "amount" column which would allow users to specify the amount of the selected product. We'll choose a Dropdown column with predefined numeric choices.

SurveyJS: Configure Amount Column which displays Numeric Choices

Configure Rows

Define Minimum and Maximum Row Number

Users can add an unlimited number of records within a Dynamic Matrix. However, to make your form easy to manage, and in our case, to avoid having more rows than the number of available coffee types, you may wish to specify the minimum and maximum number of allowed rows using the following settings in the General category.

  • Minimum row count
    Defines the minimum row count. When the number of matrix rows reaches the specified minimum, the 'Remove Row' button stops being visible, thereby preventing users from removing any of the remaining rows.

  • Maximum row count
    Defines the maximum row count. When the number of matrix rows reaches the specified maximum, the 'Add New Row' button becomes invisible, thereby restricting users from adding additional rows.

Define the Initial Row Number

By default, a matrix renders two rows. If you wish to increase or decrease the number of rows which initially appear on screen, use the General | Row count property. Note that this value should not be less than the Minimum row count and greater than the Maximum row count.

For our calculator form, these settings are defined as follows:

SurveyJS: Configure Dynamic Matrix Row Settings

Set the Default Row Value

By default, newly-added rows display empty cell values. You may wish to predefine row values to make it easier for users to fill out the table. Select the matrix and navigate to the Data category of the Property Grid.

SurveyJS: Predefine Row Values for Dynamic Matrix

Click the Change Default row value button to activate the editor and specify default cell values for the newly-added matrix rows.

SurveyJS: Specify Default Row Values

Calculate Matrix Row Totals

To aggregate values within table rows, select a matrix, navigate to the Columns category of the Property grid and create an Expression column. Define an expression which aggregates the current row values.

Within the expression, you can access individual row cell values using the row prefix as follows: {row.columnName}. For our Coffee Order form, we need to create the "Total" column ("totalPerRow") which would calculate the total price for the selected coffee item based on its price and quantity. In order to do that, we set the "Total" column's expression that multiplies the Price column value by the "Amount" column value.

{row.price} * {row.amount}
SurveyJS: Calculate the amount per row

Calculate Matrix Column Totals

Matrix column totals appear at the bottom of a matrix and display aggregated values of matrix columns. To configure column totals, select a matrix column and in the Property grid navigate to the Totals category.

SurveyJS: Calculate Amount Column Total

Total type

The Total type setting specifies the aggregation method over the selected column's values. The following data aggregation methods are available: "sum", "count", "min", "max", "avg".

When you select a predefined total, a matrix question automatically creates an expression fields to calculate the total value. Each data aggregation type internally uses one of the corresponding functions: sumInArray, countInArray, minInArray, maxInArray, avgInArray.

For the Amount column, select the "sum" summary type to calculate the total amount of selected items:

SurveyJS: Calculate Matrix Totals

Total value display style

The Total value display style property allows you to configure the display format for the total row value. Available options are: "none" (default), "decimal", "currency", "percent".

If you select any option except "none", a Form Library will use the toLocaleString function to format a total value according to the conventions of a user browser's specified locale.

For the Amount column, keep the "none" setting unchanged to display a total value without any specific formatting.

Currency

The "Currency" setting specifies a currency used to display calculated total values. This setting only applies if Total display style is set to "currency".

Formatted string

A string pattern is used to display column totals. To reference a total value within this pattern, use the {0} placeholder. The Amount column total value will be preset with the 'Total' label. The "Formatted string" property is set as follows:

Total: {0}
SurveyJS: Set the total format pattern

Total Expression

If none of the predefined Total types suits your needs, you can configure the Total Expression property using any arithmetic expression or function. For instance, to calculate the total of the Amount matrix column, you can choose the 'sum' Total type or use the sumInArray function. In both cases a total will display the whole amount or number of the selected column values.

An example of how to calculate the total amount using the built-in 'sum' data aggregation method:

{
  "name": "amount",
  "title": "Amount",
  "cellType": "dropdown",
  "isRequired": true,
  "totalType": "sum",
  "totalFormat": "Total: {0}",
  "choices": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ]
}

An example of how to calculate the total amount using a custom expression and the sumInArray function:

{
  "name": "amount",
  "title": "Amount",
  "cellType": "dropdown",
  "isRequired": true,
  "totalExpression": "'Total: ' + sumInArray({coffeeOrderTable}, 'amount')",
  "choices": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ]
}

The totalExpression property offers a great deal of flexibility, enabling you to use other functions, including a conditional iif expression, to calculate the total value. For more information on expression syntax, visit our dedicated Expressions page.

The Total Expression overrides the Total type setting.

Create Additional Summary Fields

You may want to create additional summary form fields. The Expression form element is a common way to calculate read-only row/column totals as well as row/column grand totals.

If you require to use a matrix total value within form expressions, use the {matrixname-total.columnName} syntax to reference a specific matrix column totals in an expression.

In the current demo, we create an Additional Charges section to display the Tax rate, the total amount of the tax and the total order price.

Create a Tax numeric input field and specify the value limit to be up to 20.

SurveyJS: Configure Tax Rate

Create a Tax read-only field. It will use the following expression to calculate the tax amount:

{coffeeOrderTable-total.totalPerRow} * {taxRate} / 100
SurveyJS: Configure Tax Amount

A Total read-only field displays the total order price. It summarizes the order form total with the tax amount value.

{coffeeOrderTable-total.totalPerRow} + {taxAmount}
SurveyJS: Setup Total Value Expression

Create a Custom Theme

In this demo, we created and applied a customized coffee-related theme that features an advanced header style, an image background, Tahoma font, and a reduced form scale factor. To learn how to configure a SurveyJS Creator for creating custom themes, visit the Theme Editor.

SurveyJS: Theme Editor

Sources

Below are survey and theme JSON definitions of our dynamic order form with calculated total fields.

Survey JSON Schema

{
  title: "Coffee Order Form",
  completedHtml:
    "<div style=\"background-color: #F8D3B4; padding: 20px; border-radius: 10px; font-family: 'Tahoma', sans-serif; font-size: 32px;\">\n    <p>Thank You for Your Order!</p>\n    <p>Our team is on it, making sure everything is just right for you.</p>\n</div>\n",
  pages: [
    {
      name: "page1",
      elements: [
        {
          type: "matrixdynamic",
          name: "coffeeOrderTable",
          title: "Select Your Coffee",
          description:
            "Place your coffee order with ease using the form below. Select your preferred coffee, specify the quantity, and let us handle the rest.",
          validators: [
            {
              type: "expression",
            },
          ],
          columns: [
            {
              name: "coffee",
              title: "Coffee",
              cellType: "dropdown",
              isRequired: true,
              isUnique: true,
              choices: [
                {
                  value: "espresso",
                  text: "Espresso",
                },
                {
                  value: "ristretto",
                  text: "Ristretto",
                },
                {
                  value: "macchiato",
                  text: "Macchiato",
                },
                {
                  value: "flatWhite",
                  text: "Flat White",
                },
                {
                  value: "cappuchino",
                  text: "Cappuchino",
                },
                {
                  value: "latte",
                  text: "Latte",
                },
              ],
            },
            {
              name: "price",
              title: "Price",
              cellType: "expression",
              expression:
                "iif({row.coffee} = 'ristretto' or {row.coffee} = 'macchiato' or {row.coffee} = 'cappuchino', '2.5', iif({row.coffee} = 'flatWhite' or {row.coffee} = 'latte', 3, 2))\n",
            },
            {
              name: "amount",
              title: "Amount",
              cellType: "dropdown",
              isRequired: true,
              totalExpression:
                "'Total: ' + sumInArray({coffeeOrderTable}, 'amount')",
              choices: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
            },
            {
              name: "totalPerRow",
              title: "Total",
              cellType: "expression",
              totalType: "sum",
              totalFormat: "Total: {0}",
              totalDisplayStyle: "currency",
              expression: "{row.price} * {row.amount}",
            },
          ],
          rowCount: 1,
          maxRowCount: 6,
          defaultRowValue: {
            coffeeItem: "2",
            coffee: "espresso",
            price: 2,
            amount: 1,
            totalPerRow: 2,
          },
          addRowLocation: "bottom",
          addRowText: "Add Coffee",
        },
        {
          type: "panel",
          name: "panel1",
          elements: [
            {
              type: "text",
              name: "taxRate",
              title: "TAX (in %)",
              defaultValue: 10,
              inputType: "number",
              min: 0,
              max: 20,
            },
            {
              type: "expression",
              name: "taxAmount",
              startWithNewLine: false,
              title: "TAX",
              expression:
                "{coffeeOrderTable-total.totalPerRow} * {taxRate} / 100",
              displayStyle: "currency",
            },
            {
              type: "expression",
              name: "total",
              startWithNewLine: false,
              title: "Total",
              expression: "{coffeeOrderTable-total.totalPerRow} + {taxAmount}",
              displayStyle: "currency",
            },
          ],
          title: "Additional Charges",
        },
      ],
    },
  ],
  showQuestionNumbers: "off",
  questionErrorLocation: "bottom",
  completeText: "Place Order",
  headerView: "advanced",
}

Theme JSON Schema

{
  backgroundImage:
    "https://api.surveyjs.io/private/Surveys/files?name=2c40108f-2a8b-414a-95ab-4be04dd50a77",
  backgroundImageFit: "cover",
  backgroundImageAttachment: "fixed",
  backgroundOpacity: 0.75,
  themeName: "contrast",
  colorPalette: "light",
  isPanelless: false,
  cssVariables: {
    "--sjs-general-backcolor": "rgba(255, 255, 255, 1)",
    "--sjs-general-backcolor-dark": "rgba(255, 216, 77, 1)",
    "--sjs-general-backcolor-dim": "#7f4929",
    "--sjs-general-backcolor-dim-light": "rgba(245, 220, 199, 1)",
    "--sjs-general-backcolor-dim-dark": "rgba(255, 216, 77, 1)",
    "--sjs-general-forecolor": "rgba(0, 0, 0, 1)",
    "--sjs-general-forecolor-light": "rgba(0, 0, 0, 1)",
    "--sjs-general-dim-forecolor": "rgba(0, 0, 0, 1)",
    "--sjs-general-dim-forecolor-light": "rgba(0, 0, 0, 1)",
    "--sjs-primary-backcolor": "rgba(0, 0, 0, 1)",
    "--sjs-primary-backcolor-light": "rgba(199, 168, 143, 1)",
    "--sjs-primary-backcolor-dark": "rgba(83, 83, 83, 1)",
    "--sjs-primary-forecolor": "rgba(255, 255, 255, 1)",
    "--sjs-primary-forecolor-light": "rgba(255, 255, 255, 0.25)",
    "--sjs-base-unit": "4.8px",
    "--sjs-corner-radius": "15px",
    "--sjs-secondary-backcolor": "rgba(255, 152, 20, 1)",
    "--sjs-secondary-backcolor-light": "rgba(255, 152, 20, 0.1)",
    "--sjs-secondary-backcolor-semi-light": "rgba(255, 152, 20, 0.25)",
    "--sjs-secondary-forecolor": "rgba(255, 255, 255, 1)",
    "--sjs-secondary-forecolor-light": "rgba(255, 255, 255, 0.25)",
    "--sjs-shadow-small": "0px 0px 0px 0px rgba(0, 0, 0, 1)",
    "--sjs-shadow-medium": "0px 0px 0px 2px rgba(0, 0, 0, 1)",
    "--sjs-shadow-large": "0px 6px 0px 0px rgba(0, 0, 0, 1)",
    "--sjs-shadow-inner":
      "0px 2px 0px 0px rgba(0, 0, 0, 1), 0px 0px 0px 2px rgba(0, 0, 0, 1)",
    "--sjs-shadow-inner-reset":
      "0px 0px 0px 0px rgba(0, 0, 0, 1), 0px 0px 0px 0px rgba(0, 0, 0, 1)",
    "--sjs-border-light": "rgba(0, 0, 0, 0.2)",
    "--sjs-border-default": "rgba(0, 0, 0, 1)",
    "--sjs-border-inside": "rgba(0, 0, 0, 0.16)",
    "--sjs-special-red": "rgba(229, 10, 62, 1)",
    "--sjs-special-red-light": "rgba(229, 10, 62, 0.1)",
    "--sjs-special-red-forecolor": "rgba(255, 255, 255, 1)",
    "--sjs-special-green": "rgba(25, 179, 148, 1)",
    "--sjs-special-green-light": "rgba(25, 179, 148, 0.1)",
    "--sjs-special-green-forecolor": "rgba(255, 255, 255, 1)",
    "--sjs-special-blue": "rgba(67, 127, 217, 1)",
    "--sjs-special-blue-light": "rgba(67, 127, 217, 0.1)",
    "--sjs-special-blue-forecolor": "rgba(255, 255, 255, 1)",
    "--sjs-special-yellow": "rgba(255, 152, 20, 1)",
    "--sjs-special-yellow-light": "rgba(255, 152, 20, 0.1)",
    "--sjs-special-yellow-forecolor": "rgba(255, 255, 255, 1)",
    "--sjs-article-font-xx-large-textDecoration": "none",
    "--sjs-article-font-xx-large-fontWeight": "700",
    "--sjs-article-font-xx-large-fontStyle": "normal",
    "--sjs-article-font-xx-large-fontStretch": "normal",
    "--sjs-article-font-xx-large-letterSpacing": "0",
    "--sjs-article-font-xx-large-lineHeight": "64px",
    "--sjs-article-font-xx-large-paragraphIndent": "0px",
    "--sjs-article-font-xx-large-textCase": "none",
    "--sjs-article-font-x-large-textDecoration": "none",
    "--sjs-article-font-x-large-fontWeight": "700",
    "--sjs-article-font-x-large-fontStyle": "normal",
    "--sjs-article-font-x-large-fontStretch": "normal",
    "--sjs-article-font-x-large-letterSpacing": "0",
    "--sjs-article-font-x-large-lineHeight": "56px",
    "--sjs-article-font-x-large-paragraphIndent": "0px",
    "--sjs-article-font-x-large-textCase": "none",
    "--sjs-article-font-large-textDecoration": "none",
    "--sjs-article-font-large-fontWeight": "700",
    "--sjs-article-font-large-fontStyle": "normal",
    "--sjs-article-font-large-fontStretch": "normal",
    "--sjs-article-font-large-letterSpacing": "0",
    "--sjs-article-font-large-lineHeight": "40px",
    "--sjs-article-font-large-paragraphIndent": "0px",
    "--sjs-article-font-large-textCase": "none",
    "--sjs-article-font-medium-textDecoration": "none",
    "--sjs-article-font-medium-fontWeight": "700",
    "--sjs-article-font-medium-fontStyle": "normal",
    "--sjs-article-font-medium-fontStretch": "normal",
    "--sjs-article-font-medium-letterSpacing": "0",
    "--sjs-article-font-medium-lineHeight": "32px",
    "--sjs-article-font-medium-paragraphIndent": "0px",
    "--sjs-article-font-medium-textCase": "none",
    "--sjs-article-font-default-textDecoration": "none",
    "--sjs-article-font-default-fontWeight": "400",
    "--sjs-article-font-default-fontStyle": "normal",
    "--sjs-article-font-default-fontStretch": "normal",
    "--sjs-article-font-default-letterSpacing": "0",
    "--sjs-article-font-default-lineHeight": "28px",
    "--sjs-article-font-default-paragraphIndent": "0px",
    "--sjs-article-font-default-textCase": "none",
    "--sjs-font-headertitle-color": "rgba(0, 0, 0, 1)",
    "--sjs-font-headerdescription-color": "rgba(32, 32, 32, 1)",
    "--sjs-question-background": "rgba(255, 255, 255, 1)",
    "--sjs-questionpanel-hovercolor": "rgba(199, 168, 143, 1)",
    "--sjs-editor-background": "rgba(245, 220, 199, 1)",
    "--sjs-editorpanel-backcolor": "rgba(245, 220, 199, 1)",
    "--sjs-editorpanel-hovercolor": "rgba(207, 187, 170, 1)",
    "--sjs-font-size": "16px",
    "--sjs-font-family": "Tahoma, sans-serif",
    "--sjs-font-pagetitle-color": "rgba(243, 239, 236, 1)",
    "--sjs-font-pagetitle-size": "28px",
    "--sjs-font-questiontitle-color": "rgba(0, 0, 0, 1)",
    "--sjs-font-questiontitle-size": "15px",
    "--sjs-font-questiondescription-color": "rgba(0, 0, 0, 1)",
    "--sjs-font-questiondescription-size": "15px",
    "--sjs-header-backcolor": "transparent",
  },
  header: {
    height: 100,
    inheritWidthFrom: "container",
    textAreaWidth: 512,
    overlapEnabled: false,
    backgroundImageOpacity: 1,
    backgroundImageFit: "cover",
    logoPositionX: "right",
    logoPositionY: "top",
    titlePositionX: "left",
    titlePositionY: "bottom",
    descriptionPositionX: "left",
    descriptionPositionY: "bottom",
  },
  headerView: "advanced",
}

For more information on how to add and run SurveyJS forms in your application and apply a custom theme, please visit our documentation.

Your cookie settings

We use cookies on our site to make your browsing experience more convenient and personal. In some cases, they are essential to making the site work properly. By clicking "Accept All", you consent to the use of all cookies in accordance with our Terms of Use & Privacy Statement. However, you may visit "Cookie settings" to provide a controlled consent.

Your renewal subscription expires soon.

Since the license is perpetual, you will still have permanent access to the product versions released within the first 12 month of the original purchase date.

If you wish to continue receiving technical support from our Help Desk specialists and maintain access to the latest product updates, make sure to renew your subscription by clicking the "Renew" button below.

Your renewal subscription has expired.

Since the license is perpetual, you will still have permanent access to the product versions released within the first 12 month of the original purchase date.

If you wish to continue receiving technical support from our Help Desk specialists and maintain access to the latest product updates, make sure to renew your subscription by clicking the "Renew" button below.