Nested Forms in SurveyJS — Add Readable Summaries to Repeatable Subforms (Part 2)

A supplier may ship dozens of SKUs. Asking for each product on its own page is tedious; dumping every field in one long block is hard to review. What respondents need is a compact list of what they already entered with the option to expand an item, fix a typo, or remove a line.

SurveyJS does this with entry title templates on a Dynamic Panel. Each nested entry shows a short summary (for example, Powder-coated shelving unit · set · 21d · €340). This provides a scannable list for data entry and form review—not a server-backed data grid with filtering, sorting, or record lookup.

Nested forms in SurveyJS: repeatable subforms with readable tab summaries

In This Series

This three-part guide uses a supplier registration example to show how to build nested forms in SurveyJS:

Example

Try the following live demo. Add product lines in tab mode. Switch between tabs, watch titles update from New product line to a compact summary with unit, lead time, and price.

Open Demo in StackBlitz

Business Scenario: Product Catalog Lines

Continuing our supplier onboarding flow, the purchasing team also needs product or service lines the vendor can supply:

  • Product name
  • Unit of measure
  • List price
  • Lead time (days)

Users add lines one by one. In tab mode, each line appears as its own tab. After a few entries, tab titles might look like this:

  • ISO 4762 M8 bolt kit · pcs · 5d · €2.40
  • Powder-coated shelving unit · set · 21d · €340
  • Annual PM contract (site visit) · hour · 1d · €95
  • Custom CNC machining · kg · 14d · €18.50

Before a product name is entered, the tab shows a placeholder such as New product line. As the respondent fills in unit, lead time, and price, the title grows into the full summary—without opening each tab to see what it contains.

Survey JSON

Enable tab mode with "displayMode": "tab", then bind templateTabTitle to a hidden Expression question. The expression builds a compact summary and falls back to a placeholder when {panel.productName} is empty.

{
  "title": "Supplier Registration — Product lines",
  "pages": [
    {
      "name": "products",
      "title": "Products & services",
      "description": "List each product or billable service you can supply to us. Add one line per item.",
      "elements": [
        {
          "type": "paneldynamic",
          "name": "productLines",
          "title": "Catalog lines",
          "templateElements": [
            {
              "type": "expression",
              "name": "tabTitle",
              "visible": false,
              "expression": "iif({panel.productName} empty, 'New product line', {panel.productName} + ' · ' + {panel.unit} + ' · ' + {panel.leadTimeDays} + 'd · €' + {panel.listPrice})"
            },
            {
              "type": "text",
              "name": "productName",
              "title": "Product or service name",
              "isRequired": true
            },
            {
              "type": "dropdown",
              "name": "unit",
              "title": "Unit",
              "isRequired": true,
              "choices": [
                "pcs",
                "set",
                "kg",
                "hour"
              ]
            },
            {
              "type": "text",
              "name": "listPrice",
              "title": "List price (EUR)",
              "isRequired": true,
              "inputType": "number",
              "min": 0
            },
            {
              "type": "text",
              "name": "leadTimeDays",
              "title": "Lead time (days)",
              "isRequired": true,
              "inputType": "number",
              "min": 0
            }
          ],
          "templateTitle": "{panel.tabTitle}",
          "templateTabTitle": "{panel.tabTitle}",
          "templateDescription": "Per {panel.unit} · {panel.leadTimeDays} calendar days lead time",
          "panelCount": 1,
          "minPanelCount": 1,
          "maxPanelCount": 20,
          "confirmDelete": true,
          "confirmDeleteText": "Remove this product line?",
          "addPanelText": "Add product line",
          "removePanelText": "Remove line",
          "displayMode": "tab"
        },
        {
          "type": "expression",
          "name": "lineCount",
          "title": "Lines added",
          "expression": "countInArray({productLines}, 'productName')"
        }
      ]
    }
  ]
}

Tab Titles with Placeholders

The approach follows the tabbed Dynamic Panel demo:

  1. Add a hidden Expression question ("visible": false) inside templateElements.
  2. Use iif({panel.productName} empty, 'New product line', ...) to return a placeholder or the full summary.
  3. Set both templateTabTitle and templateTitle to {panel.tabTitle} so tabs and entry headers stay in sync.

Use {panelIndex} in the placeholder if you prefer numbered tabs: 'Product line #' + {panelIndex}.

Other Summary Formats

The same hidden Expression approach works for other nested records. Choose a format that matches how your team reads the data:

Pattern Expression idea Example tab title
Name + role {panel.fullName} + ' · ' + {panel.role} Elena Rossi · Procurement lead
Name + price only {panel.productName} + ' — €' + {panel.listPrice} Stainless hinge — €12.80
Name + quantity + unit {panel.productName} + ' · ' + {panel.moq} + ' ' + {panel.unit} + ' MOQ' Cable gland · 500 pcs MOQ
Conditional prefix iif({panel.inStock} = 'yes', 'In stock · ', 'Backorder · ') + {panel.productName} In stock · ISO 4762 M8 bolt kit

Keep separators consistent (· or ) so tabs align visually when users scan a long catalog.

Simpler Option: One Field in the Tab Title

When the tab title uses a single question—{panel.productName}—you do not need a hidden Expression. Set templateTabTitle to {panel.productName} and tabTitlePlaceholder to New product line on the Dynamic Panel. The placeholder appears until the user enters a product name.

{
  "type": "paneldynamic",
  "name": "productLines",
  "templateTabTitle": "{panel.productName}",
  "tabTitlePlaceholder": "New product line",
  "templateElements": [
    {
      "type": "text",
      "name": "productName",
      "title": "Product or service name"
    }
  ]
}

Use the hidden Expression from the main example when the tab title combines several fields (name · unit · lead time · price) or needs conditional logic.

Display Modes for Long Nested Sections

For many similar entries, choose a display mode that reduces clutter:

Mode Best for
List (default) Short lists; several expanded sections at once
Tabs Medium lists; one entry visible, quick switching (used in the JSON above)
Carousel Mobile-friendly step-through of entries

All three modes support summarized titles. Pick based on how many fields each nested record contains—not on reporting needs.

Optional: Total at the Parent Level

You can show simple aggregates on the parent form using expressions. To count catalog lines, use the countInArray function with the Dynamic Panel question and the template field name:

countInArray(question, valueField, filter?)

  • question – The panel array, for example {productLines}.
  • valueField – A question name inside templateElements, for example 'productName'.
  • filter (optional) – A Boolean expression evaluated per panel row.

Example: "expression": "countInArray({productLines}, 'productName')" counts panels where productName is not empty.

For price rollups per line, store numeric values and sum in your server or use calculated fields when appropriate.

Sample Result JSON

{
  "productLines": [
    {
      "productName": "ISO 4762 M8 bolt kit",
      "unit": "pcs",
      "listPrice": 2.4,
      "leadTimeDays": 5
    },
    {
      "productName": "Powder-coated shelving unit",
      "unit": "set",
      "listPrice": 340,
      "leadTimeDays": 21
    },
    {
      "productName": "Annual PM contract (site visit)",
      "unit": "hour",
      "listPrice": 95,
      "leadTimeDays": 1
    }
  ],
  "lineCount": 3
}

Design Tips

  • Keep nested templates short — four to six fields per subform entry works well.
  • Put the identifier first — use a hidden Expression for tab titles when the summary combines several fields or needs a placeholder.
  • Set minPanelCount to match policy — require at least one catalog line before submit.
  • Use confirmDelete on financial or compliance data — prevents accidental loss of a line item.

What's Next

Part 3 covers conditional nested sections: show a Safety Data Sheet (SDS) subform with file uploads only when the supplier handles hazardous materials.

Your cookie settings

We use cookies to make your browsing experience more convenient and personal. Some cookies are essential, while others help us analyse traffic. Your personal data and cookies may be used for ad personalization. By clicking “Accept All”, you consent to the use of all cookies as described in our Terms of Use and Privacy Statement. You can manage your preferences in “Cookie settings.”

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.