---
title: Double-Entry Verification
product: Form Library
description: Compare two independent data entries in SurveyJS forms to identify data discrepancies. View this JavaScript demo to learn more.
framework: Angular
source: https://surveyjs.io/form-library/examples/double-entry-verification/angular
index: https://surveyjs.io/form-library/examples/overview.md
---

# Double-Entry Verification (Angular)

Double-entry verification—also known as double data entry or dual data entry—is a data quality control process in which the same information is entered independently twice, and the two entries are then compared to identify discrepancies. Its purpose is to detect human transcription errors, particularly when data is captured from paper forms, scanned documents, handwritten notes, or other source records. Unlike ordinary validation, which checks whether a value is valid, double-entry verification checks whether it was entered accurately. For example, the value `27` passes a numeric validator even if the source document says `72`.

A typical double-entry verification workflow includes two validation layers:

- Assisted confirmation\
Email and confirm email are compared with a client-side expression validator. In this demo, validators use [`"notificationType": "warning"`](/form-library/documentation/api-reference/textvalidator#notificationType), so validation messages do not block form submission. To prevent the form from being submitted until the issue is resolved, use the default `"error"` notification type. See [Validator Notification Types](/survey-creator/documentation/end-user-guide/data-validation-in-forms#validator-notification-types).

- Selective dual entry\
Critical fields are entered again on the second page and compared asynchronously in the [`onServerValidateQuestions`](/form-library/documentation/surveymodel#onServerValidateQuestions) event, which can return a custom validation message for each field.

## How This Demo Works

1. First entry\
Enter the participant ID, weight, height, date of birth, and email, or click **Fill Sample Data**.

2. Assisted check\
Confirm the email address. An expression validator checks that both email fields match.

3. Second entry\
Re-enter the critical fields without seeing the first-entry values, or click **Fill Sample Data**.

4. Server-side comparison\
When you complete the form, `onServerValidateQuestions` compares the paired values and reports mismatches as field-level errors.

## Connect to an API Endpoint

In regulated environments, a second operator often compares their entries with the first operator's saved submission on the server. To implement this workflow, call your comparison endpoint in `onServerValidateQuestions`, populate the `errors` object, and then call `complete()`:


```js
function validateDoubleEntryAsync(sender, { data, errors, complete }) {
  fetch("/api/validation/double-entry", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      // Prefer sender.data so first-entry values are available when only page 2 is validated
      participantId: sender.data.participantId,
      participantId2: data.participantId2,
      weightKg: sender.data.weightKg,
      weightKg2: data.weightKg2
      // ...other paired fields
    })
  })
    .then((response) => response.json())
    .then((result) => {
      // result.errors: { weightKg2: "Mismatch with first entry..." }
      Object.keys(result.errors || {}).forEach((name) => {
        errors[name] = result.errors[name];
      });
      complete();
    })
    .catch(() => {
      errors["weightKg2"] = "Comparison service is temporarily unavailable. Try again.";
      complete();
    });
}

survey.onServerValidateQuestions.add(validateDoubleEntryAsync);
```

## Related Examples

- [Server-Side Form Validation Using Expressions](/form-library/examples/javascript-async-form-validation/)
- [Server-Side Form Validation Using an Event](/form-library/examples/javascript-server-side-form-validation/)
- [Detect Mis-Keyed Form Entries](/form-library/examples/detect-miskeyed-form-entries/)
- [Detect Duplicate Form Records](/form-library/examples/detect-duplicate-form-records/)

## Files

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

```ts
export const json = {
  "title": "Patient Measurements — Double-Entry Check",
  "description": "Enter critical values, confirm the email address on page 1, then re-enter the critical fields on page 2. The second entry is compared with the first when you submit the form.",
  "checkErrorsMode": "onValueChanged",
  "completeText": "Submit",
  "pages": [
    {
      "name": "firstEntry",
      "title": "First entry",
      "description": "Enter values from the source document.",
      "elements": [
        {
          "type": "text",
          "name": "participantId",
          "title": "Participant ID",
          "isRequired": true,
          "placeholder": "e.g. TRIAL-1042"
        },
        {
          "type": "text",
          "name": "weightKg",
          "title": "Weight (kg)",
          "isRequired": true,
          "maskType": "numeric",
          "maskSettings": {
            "min": 0,
            "precision": 1
          },
          "placeholder": "e.g. 72",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "heightCm",
          "title": "Height (cm)",
          "isRequired": true,
          "maskType": "numeric",
          "maskSettings": {
            "min": 0,
            "precision": 0
          },
          "placeholder": "e.g. 180",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "dateOfBirth",
          "title": "Date of birth",
          "isRequired": true,
          "inputType": "date"
        },
        {
          "type": "text",
          "name": "email",
          "title": "Email",
          "isRequired": true,
          "inputType": "email",
          "validators": [{ "type": "email" }]
        },
        {
          "type": "text",
          "name": "emailConfirm",
          "title": "Confirm email",
          "description": "Assisted verification — client-side expression checks that both emails match.",
          "isRequired": true,
          "inputType": "email",
          "validators": [
            { "type": "email" },
            {
              "type": "expression",
              "text": "Email addresses do not match.",
              "expression": "{email} empty or {emailConfirm} empty or {email} = {emailConfirm}",
              "notificationType": "warning"
            }
          ]
        }
      ]
    },
    {
      "name": "secondEntry",
      "title": "Second entry",
      "description": "Re-enter the same values without referring to the first entry.",
      "elements": [
        {
          "type": "text",
          "name": "participantId2",
          "title": "Participant ID",
          "isRequired": true,
          "placeholder": "Re-enter value",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "weightKg2",
          "title": "Weight (kg)",
          "isRequired": true,
          "maskType": "numeric",
          "maskSettings": {
            "min": 0,
            "precision": 1
          },
          "placeholder": "Re-enter value",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "heightCm2",
          "title": "Height (cm)",
          "isRequired": true,
          "maskType": "numeric",
          "maskSettings": {
            "min": 0,
            "precision": 0
          },
          "placeholder": "Re-enter value",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "dateOfBirth2",
          "title": "Date of birth",
          "isRequired": true,
          "inputType": "date"
        },
        {
          "type": "html",
          "name": "mismatchSummary",
          "html": "<p><em>Click <strong>Submit</strong> to compare both entries. Any mismatches are displayed next to the corresponding fields.</em></p>"
        }
      ]
    }
  ],
  "width": "750px"
}
;
```

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

```css
.nav-button {
  margin-left: auto;
  margin-right: 0;
}
```

### `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";

// Assisted email confirm stays client-side; dual-entry pairs use onServerValidateQuestions.
// See the Documentation tab to connect a real comparison API (Operator A/B, audit trail, locking).

const DOUBLE_ENTRY_PAIRS = [
  { first: "participantId", second: "participantId2", label: "Participant ID" },
  { first: "weightKg", second: "weightKg2", label: "Weight (kg)" },
  { first: "heightCm", second: "heightCm2", label: "Height (cm)" },
  { first: "dateOfBirth", second: "dateOfBirth2", label: "Date of birth" }
];

function normalizeComparable(value) {
  if (value === undefined || value === null) {
    return "";
  }
  return String(value).trim();
}

function valuesMatch(left, right) {
  const a = normalizeComparable(left);
  const b = normalizeComparable(right);
  if (a === "" || b === "") {
    return true;
  }
  const aNum = Number(a);
  const bNum = Number(b);
  if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && a !== "" && b !== "") {
    if (String(a).match(/^-?\d+(\.\d+)?$/) && String(b).match(/^-?\d+(\.\d+)?$/)) {
      return aNum === bNum;
    }
  }
  return a.toLowerCase() === b.toLowerCase();
}

// In-demo async comparison (replace with fetch to your comparison API in production).
function mockValidationApi(payload) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const errors = {};

      DOUBLE_ENTRY_PAIRS.forEach((pair) => {
        const firstValue = payload[pair.first];
        const secondValue = payload[pair.second];
        if (!normalizeComparable(firstValue) || !normalizeComparable(secondValue)) {
          return;
        }
        if (valuesMatch(firstValue, secondValue)) {
          return;
        }
        errors[pair.second] =
          `Mismatch with first entry for ${pair.label}: entered "${normalizeComparable(secondValue)}" ` +
          `vs first entry "${normalizeComparable(firstValue)}". Re-check the source document.`;
      });

      resolve({ errors });
    }, 450);
  });
}

function validateDoubleEntryAsync(sender, { data, errors, complete }) {
  const surveyData = sender.data || {};
  const payload = {};
  DOUBLE_ENTRY_PAIRS.forEach((pair) => {
    payload[pair.first] = surveyData[pair.first];
    payload[pair.second] =
      data[pair.second] !== undefined ? data[pair.second] : surveyData[pair.second];
  });

  mockValidationApi(payload)
    .then((result) => {
      Object.keys(result.errors || {}).forEach((name) => {
        errors[name] = result.errors[name];
      });
      complete();
    })
    .catch(() => {
      errors["weightKg2"] = "Comparison service is temporarily unavailable. Try again.";
      complete();
    });
}

@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.onServerValidateQuestions.add(validateDoubleEntryAsync);
        
        const FIRST_ENTRY_SAMPLE = {
          participantId: "TRIAL-1042",
          weightKg: 72,
          heightCm: 180,
          dateOfBirth: "1987-04-12",
          email: "patient@example.com",
          emailConfirm: "patient@example.com"
        };
        
        const SECOND_ENTRY_SAMPLE = {
          participantId2: "TRIAL-1042",
          weightKg2: 27,
          heightCm2: 180,
          dateOfBirth2: "1987-04-12"
        };
        
        survey.addNavigationItem({
          id: "sv-nav-fill-sample",
          title: "Fill Sample Data",
          action: () => {
            const sample =
              survey.currentPage.name === "secondEntry"
                ? SECOND_ENTRY_SAMPLE
                : FIRST_ENTRY_SAMPLE;
            Object.keys(sample).forEach((name) => {
              survey.setValue(name, sample[name]);
            });
          },
          css: "nav-button",
          innerCss: "sd-btn"
        });
        
        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
.nav-button {
  margin-left: auto;
  margin-right: 0;
}
```

### `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/double-entry-verification/reactjs.md)
- [Vue 3](https://surveyjs.io/form-library/examples/double-entry-verification/vue3js.md)
- [jQuery](https://surveyjs.io/form-library/examples/double-entry-verification/jquery.md)
- [Vanilla JS](https://surveyjs.io/form-library/examples/double-entry-verification/vanillajs.md)
