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.
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.
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.
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.
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.
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))
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.
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:
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.
Click the Change Default row value
button to activate the editor and specify default cell values for the newly-added matrix rows.
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}
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.
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:
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}
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.
Create a Tax
read-only field. It will use the following expression to calculate the tax amount:
{coffeeOrderTable-total.totalPerRow} * {taxRate} / 100
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}
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.
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.