---
title: Tabbed Interface for Duplicate Group of Fields
product: Form Library
description: With SurveyJS, you can set up dynamic forms with groups of fields that repeat. Enable tabs render mode to display tabs for each duplicate group entry. Check our this free JavaScript demo showcasing the duplicate group option to learn more.
framework: Angular
source: https://surveyjs.io/form-library/examples/tabbed-interface-for-duplicate-group-option/angular
index: https://surveyjs.io/form-library/examples/overview.md
---

# Tabbed Interface for Duplicate Group of Fields (Angular)

A Dynamic Panel displays a group of repeating form fields that respondents can duplicate on the fly. Dynamic Panels support a tabbed interface, where users can switch between duplicate groups using tabs. This demo shows how to enable and configure the tabbed UI.

## Create a Dynamic Panel

For information on how to create and configure a Dynamic Panel, refer to the following demo:

[Dynamic Panel with Repeating Group of Fields](/form-library/examples/duplicate-group-of-fields-in-form/ (linkStyle))

## Enable the Tab View and Configure Tab Titles

To enable the tab view, set the [`displayMode`](https://surveyjs.io/form-library/documentation/api-reference/dynamic-panel-model#displayMode) property to `"tab"`. Once the tabbed UI is activated, specify a custom template for tab titles using the [`templateTabTitle`](/form-library/documentation/api-reference/dynamic-panel-model#templateTabTitle) property. Your template can include the `{panelIndex}` and `{visiblePanelIndex}` placeholders that contain panel indices within the collections of all or only visible panels, respectively. These indices start with 1. If you do not specify the `templateTabTitle` property, tab titles are constructed according to the following pattern: Panel 1, Panel 2, Panel 3, and so on.

In this demo, tab titles display selected countries. Until you select a country, a new tab displays the "New Vacation" title. This logic is implemented using an invisible [Expression](https://surveyjs.io/form-library/examples/expression-question-for-dynamic-form-calculations/) form field. The `templateTabTitle` property is bound to this Expression. If you want to customize an individual tab title, handle [`SurveyModel`](/form-library/documentation/api-reference/survey-data-model)'s [`onGetDynamicPanelTabTitle`](/form-library/documentation/api-reference/survey-data-model#onGetDynamicPanelTabTitle) event (not used in this demo).

## Files

### `src/app/components/json.ts`

```ts
export const json = {
  "elements": [
    {
      "type": "paneldynamic",
      "name": "vacation-info",
      "title": "Enter information about your vacations",
      "displayMode": "tab",
      "templateTabTitle": "{panel.tabTitle}",
      "panelCount": 1,
      "maxPanelCount": 10,
      "confirmDelete": true,
      "templateElements": [
        {
          "type": "expression",
          "name": "tabTitle",
          "visible": false,
          "expression": "iif({panel.country} empty, 'New Vacation', {panel.country})"
        },
        {
          "type": "dropdown",
          "name": "country",
          "title": "Select a country",
          "choicesByUrl": {
            "url": "https://surveyjs.io/api/CountriesExample",
            "valueName": "name"
          }
        },
        {
          "type": "text",
          "name": "start-date",
          "visibleIf": "{panel.country} notempty",
          "title": "Select a vacation start date",
          "defaultValueExpression": "today(-10)",
          "inputType": "date"
        },
        {
          "type": "text",
          "name": "end-date",
          "visibleIf": "{panel.country} notempty",
          "startWithNewLine": false,
          "title": "Select a vacation end date",
          "defaultValueExpression": "today()",
          "validators": [
            {
              "type": "expression",
              "text": "End date should be greater than start date",
              "expression": "{panel.start-date} < {panel.end-date}"
            }
          ],
          "inputType": "date"
        },
        {
          "type": "html",
          "name": "days-spent",
          "visibleIf": "{panel.country} notempty and {panel.start-date} notempty and {panel.end-date} notempty and {panel.start-date} < {panel.end-date}",
          "titleLocation": "left",
          "readOnly": true,
          "html": "<b>Days spent in {panel.country}</b>: {panel.diffdays}"
        },
        {
          "type": "expression",
          "name": "diffdays",
          "visible": false,
          "expression": "iif({panel.start-date} < {panel.end-date}, diffDays({panel.start-date}, {panel.end-date}), 0)"
        }
      ]
    }
  ]
};
```

### `src/app/components/survey.component.css`

```css
/* You can add your custom CSS here. */
```

### `src/app/components/survey.component.html`

```html
<survey [model]="model" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; height:100%"></survey>
```

### `src/app/components/survey.component.ts`

```ts
import { Component, OnInit } from "@angular/core";
import { Model } from "survey-core";
import { json } from "./json";
import "./survey.component.css";
import "survey-core/survey-core.min.css";

@Component({
    // tslint:disable-next-line:component-selector
    selector: "component-survey",
    templateUrl: "./survey.component.html",
    styleUrls: ["./survey.component.css"]
})
export class SurveyComponent implements OnInit {
    model: Model;
    ngOnInit() {
        const survey = new Model(json);
        survey.onGetDynamicPanelTabTitle.add((_, options) => {
            if (options.title) {
                // Customize a tab title here
            }
        });
        survey.onComplete.add((sender, options) => {
            console.log(JSON.stringify(sender.data, null, 3));
        });
        this.model = survey;
    }
}
```

### `src/app/components/theme.ts`

```ts
export const themeJson = {};
```

### `src/app/app.component.css`

```css

```

### `src/app/app.component.html`

```html
<component-survey></component-survey>
```

### `src/app/app.component.ts`

```ts
import { Component } from "@angular/core";

@Component({
    selector: "app-root",
    templateUrl: "./app.component.html",
    styleUrls: ["./app.component.css"]
})
export class AppComponent {
    title = "CodeSandbox";
}
```

### `src/app/app.module.ts`

```ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppComponent } from "./app.component";
import { SurveyModule } from "survey-angular-ui";
import { SurveyComponent } from "./components/survey.component";

@NgModule({
    declarations: [AppComponent, SurveyComponent],
    imports: [BrowserModule, SurveyModule],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }
```

### `src/environments/environment.prod.ts`

```ts
export const environment = {
    production: true
};
```

### `src/environments/environment.ts`

```ts
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.

export const environment = {
    production: false
};
```

### `src/index.html`

```html
<app-root></app-root>
```

### `src/main.ts`

```ts
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch(err => console.log(err));
```

### `src/polyfills.ts`

```ts
/**
 * This file includes polyfills needed by Angular and is loaded before the app.
 * You can add your own extra polyfills to this file.
 *
 * This file is divided into 2 sections:
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
 *      file.
 *
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
 * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
 * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
 *
 * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js';  // Run `npm install classlist.js`.

/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';

/** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import "core-js/proposals/reflect-metadata";

/**
 * Required to support Web Animations `@angular/platform-browser/animations`.
 * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
 **/
// import 'web-animations-js';  // Run `npm install web-animations-js`.

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import "zone.js/dist/zone"; // Included with Angular CLI.

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
```

### `src/styles.css`

```css
/* You can add your custom CSS here. */
```

### `src/typings.d.ts`

```ts
/* SystemJS module definition */
declare var module: NodeModule;
interface NodeModule {
    id: string;
}
```

### `.angular-cli.json`

```json
{
  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [ "assets", "favicon.ico" ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "prefix": "app",
      "styles": [ "styles.css" ],
      "scripts": [ ],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ]
}
```

### `_tsconfig.json`

```json
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "allowSyntheticDefaultImports": true,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "resolveJsonModule": true,
    "target": "es2015",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ]
  }
}
```

### `package.json`

```json
{
  "name": "surveyjs-angular",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build --prod",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "14.1.1",
    "@angular/cdk": "14.1.1",
    "@angular/common": "14.1.1",
    "@angular/compiler": "14.1.1",
    "@angular/core": "14.1.1",
    "@angular/forms": "14.1.1",
    "@angular/platform-browser": "14.1.1",
    "@angular/platform-browser-dynamic": "14.1.1",
    "@angular/router": "14.1.1",
    "core-js": "3.6.4",
    "rxjs": "6.5.4",
    "survey-core": "latest",
    "survey-angular-ui": "latest",
    "tslib": "1.13.0",
    "zone.js": "0.11.7"
  },
  "devDependencies": {
    "@angular/cli": "1.6.6",
    "@angular/compiler-cli": "^5.2.0",
    "@angular/language-service": "^5.2.0",
    "@types/core-js": "0.9.46",
    "@types/jasmine": "~2.8.3",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "~6.0.60",
    "codelyzer": "^4.0.1",
    "jasmine-core": "~2.8.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~2.0.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~4.1.0",
    "tslint": "~5.9.1",
    "typescript": "3.9.7"
  },
  "keywords": [ "angular", "surveyjs" ],
  "description": "SurveyJS-Angular example project"
}
```

## Other Frameworks

- [React](https://surveyjs.io/form-library/examples/tabbed-interface-for-duplicate-group-option/reactjs.md)
- [Vue 3](https://surveyjs.io/form-library/examples/tabbed-interface-for-duplicate-group-option/vue3js.md)
- [jQuery](https://surveyjs.io/form-library/examples/tabbed-interface-for-duplicate-group-option/jquery.md)
- [Vanilla JS](https://surveyjs.io/form-library/examples/tabbed-interface-for-duplicate-group-option/vanillajs.md)
