Utilizing a Custom Validator to Compare Two Values in a Dynamic FormArray in Angular 7

Within the "additionalForm" group, there is a formArray named "validations" that dynamically binds values to the validtionsField array. The validtionsField array contains three objects with two values that need to be compared: Min-length and Max-Length.

For example, if the min length entered is greater than the max length, an error should be displayed.

Below is the code for this functionality:

import {
  Component,
  OnInit,
  Inject
} from "@angular/core";
import {
  FormControl,
  FormArray,
  FormGroup,
  FormBuilder,
  Validators,
  AbstractControl,
  ValidationErrors,
  NgControlStatus,
  ValidatorFn
} from "@angular/forms";
import {
  MatDialogRef,
  MAT_DIALOG_DATA,
  MatSnackBar
} from "@angular/material";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
  validtionsField = [{
      validField: "Min Length",
      type: false,
      fieldType: "input",
      value: 1,
      keyval: "minLength"
    },
    {
      validField: "Max Length",
      type: false,
      fieldType: "input",
      value: 50,
      keyval: "maxLength"
    },
    {
      validField: "DataType",
      type: false,
      fieldType: "dropDown",
      dataTypeList: [],
      dataTypeId: "minLength",
      keyval: "dataTypeId",
      value: 874
    }
  ];

  dataType = [{
      id: 3701,
      localeId: 1,
      tenantId: 1,
      parentCategoryId: null,
      parentContentId: 873,
      name: "Alphabets",
      description: null
    },
    {
      id: 3702,
      localeId: 1,
      tenantId: 1,
      parentCategoryId: null,
      parentContentId: 874,
      name: "Alphanumeric",
      description: null
    }
  ];
  additionalForm: FormGroup = this.fb.group({
    fieldName: ["", [Validators.required]],
    validations: this.fb.array([])
  });

  constructor(public fb: FormBuilder) {}

  ngOnInit() {
    let frmArray = this.additionalForm.get("validations") as FormArray;

    for (let data of this.validtionsField) {
      frmArray.push(this.initSection(data));
    }
  }
  initSection(data) {
    return this.fb.group({
      validField: [data.validField, [Validators.required]],
      type: [data.type, [Validators.required]],
      value: [data.value, [Validators.required]],
      dataTypeList: [this.dataType, [Validators.required]],
      fieldType: [data.fieldType, [Validators.required]],
      validArray: []
    }, {
      validator: this.customValidator
    });
  }

  checkFieldType(data): any {
    return data === "dropDown";
  }

  // Function to compare values of min and max length
  public customValidator(control: AbstractControl): ValidationErrors | null {
    const newValue = control.get("value") ? control.get("value").value : null;
    const values = control.get("value") ? control.get("value").value : [];
    console.log("1 " + newValue);
    console.log(values);
    for (let i = 0, j = values.length; i < j; i++) {
      if (newValue === values[i]) {
        return {
          duplicate2: true
        };
      }
    }
    return null;
  }
}
<form [formGroup]="additionalForm">
  <mat-form-field>
    <input formControlName='fieldName' placeholder="Field Name" required matInput>
  </mat-form-field>
  <div class="row">
    <div class="col-md-12 col-sm-12">
      \
      <div formArrayName="validations">
        <ng-container *ngFor="let validationForm of  additionalForm.controls.validations.controls; let i = index">
          <div class="valid-data" [formGroupName]="i">
            <span>
                  <label>{{validationForm.value.validField }}</label>

                </span>
            <span>
                  <ng-container *ngIf="checkFieldType(validationForm.value.fieldType ); else input">
                    <mat-form-field class="select-dataType">
                      <mat-select required formControlName='value'  placeholder="Datatype">
                        <mat-option *ngFor="let fieldTypeData of validationForm.value.dataTypeList"
                          [value]='fieldTypeData.parentContentId'>
                          {{fieldTypeData.name}}</mat-option>
                      </mat-select>
                    </mat-form-field>
                  </ng-container>
                  <ng-template #input>
                    <mat-form-field>
                      <input required  formControlName='value' pattern= "[0-9]+" matInput>
                    </mat-form-field>
                  </ng-template>
                </span>
            <div *ngIf="validationForm.get('value')?.touched ">
              <div class="error" *ngIf="validationForm.get('value').hasError('required')">
                {{validationForm.value.validField}} is required
              </div>
            </div>
          </div>
        </ng-container>
      </div>
    </div>
  </div>
</form>

The TS and HTML code above corresponds to the function below, where I am attempting to compare old and new values from the control, but it is returning values from the same input field for the same min-length:

/// Trying the function below to compare the min and max length.

public customValidator(control: AbstractControl): ValidationErrors | null {
  const newValue = control.get('value') ? control.get('value').value : null;
  const values = control.get('value') ? control.get('value').value : [];
  console.log("1 " + newValue);
  console.log(values);
  for (let i = 0, j = values.length; i < j; i++) {
    if (newValue === values[i]) {
      return {
        'duplicate2': true
      };
    }
  }
  return null;
}

I require assistance in comparing values from a dynamic form array, where all values entered into the form-array object are bound to the formControlName "value".

Click on the following link for the code :

https://stackblitz.com/edit/angular6-material-components-demo-wgtafn

Answer №1

If you have two fields named minLength and maxLength that rely on each other for validation, you can include a validator in the parent group and utilize a custom ErrorStateMatcher to handle parent group errors for the child elements. In this scenario, using a FormGroup instead of FormArray proves to be more practical.

@Component({...})
export class AppComponent {
  ...

  readonly invalidLengthMatcher: ErrorStateMatcher = {
    isErrorState: () => {
      const control = this.additionalForm.get('validations');
      return control.hasError('invalidLength');
    }
  };

  readonly controlFields = this.validtionsField.map(field => ({
    field,
    control: new FormControl(field.value, Validators.required),
    errorMatcher: this.errorMatcherByFieldId(field.keyval)
  }));

  private readonly controlMap = this.controlFields.reduce((controls, controlField) => {
    controls[controlField.field.keyval] = controlField.control;
    return controls;
  }, {});

  readonly additionalForm = new FormGroup({
    fieldName: new FormControl("", [Validators.required]),
    validations: new FormGroup(this.controlMap, {
      validators: (group: FormGroup) => {
        const [minLength, maxLength] = ['minLength', 'maxLength'].map(fieldId => {
          const control = group.get(fieldId);
          return Number(control.value);
        });

        if (minLength > maxLength) {
          return {
            'invalidLength': true
          };
        } else {
          return null;
        }
      }
    })
  });

  private errorMatcherByFieldId(fieldId: string): ErrorStateMatcher | undefined {
    switch (fieldId) {
      case 'minLength':
      case 'maxLength':
        return this.invalidLengthMatcher;
    }
  }
}
<form [formGroup]="additionalForm">
  <mat-form-field>
    <input formControlName='fieldName' placeholder="Field Name" required matInput>
  </mat-form-field>
  <div class="row">
    <div class="col-md-12 col-sm-12">
      <div formGroupName="validations" >
        <div *ngFor="let controlField of controlFields" class="valid-data">
          <span>
            <label>{{controlField.field.validField}}</label>
          </span>
          <span [ngSwitch]="controlField.field.fieldType">
            <mat-form-field *ngSwitchCase="'dropDown'" class="select-dataType">
              <mat-select required placeholder="Datatype" [formControlName]="controlField.field.keyval">
                <mat-option *ngFor="let fieldTypeData of dataType"
                            [value]='fieldTypeData.parentContentId'>{{fieldTypeData.name}}</mat-option>
              </mat-select>
            </mat-form-field>
            <mat-form-field *ngSwitchCase="'input'">
              <input matInput
                      required
                      type="number"
                      pattern= "[0-9]+"
                      [formControlName]="controlField.field.keyval"
                      [errorStateMatcher]="controlField.errorMatcher">
            </mat-form-field>
          </span>
          ...
        </div>
      </div>
    </div>
  </div>
</form>

StackBlitz

Answer №2

To access all controls, you needed to attach this validator to the form array.

Here is My Custom Validator:-

    export function validateCustomArray(): ValidatorFn {
    return (formArray:FormArray):{[key: string]: any} | null=>{
      console.log('calling');
      let valid:boolean=true;
      setTimeout(()=>console.log(formArray),200);
      let minIndex = Object.keys(formArray.controls).findIndex((key) => formArray.controls[key].value.validField.toLowerCase()==="min length");
      let minLengthValue = formArray.controls[minIndex].value.value;
      let maxLengthValue = formArray.controls[Object.keys(formArray.controls).find((key) => formArray.controls[key].value.validField.toLowerCase()==="max length")].value.value;
      return minLengthValue < maxLengthValue ? null : {error: 'Min Length Should be less than max length', controlName : formArray.controls[minIndex].value.validField};
    }
  };

This code should be added to ngOnInit of your form array:

ngOnInit() {
    let frmArray = this.additionalForm.get("validations") as FormArray;

    for (let data of this.validtionsField) {
      frmArray.push(this.initSection(data));
    }
    frmArray.setValidators(validateCustomArray());
  }

You can utilize it in your template like this:

<div class="error" *ngIf="additionalForm.controls.validations.errors && validationForm.value.validField ===  additionalForm.controls.validations.errors.controlName">
                             {{additionalForm.controls.validations.errors.error}}
                      </div>

For a demo, check out this stackblitz link:

https://stackblitz.com/edit/angular6-material-components-demo-p1tpql

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The utilization of Angular 2 and the assignment of formControlName values within a

Why must learning Angular2 be such a challenge? <input type="text" formControlName="exposure" type="hidden"> <label>{{exposure}}</label> When I use the formControlName in the input, the value is correct. How can I retrieve the value of ...

How can the height of a Material-UI Grid item be dynamically set to match its width?

I am trying to create grid items that are square, where the height matches the width: const App = ({ width, items }) => ( <Grid container> {items.map((v) => ( <Grid item md={width}> // I want this Grid item to be square ...

Exploring the wonders of useState in React/JavaScript - a comprehensive guide

I encountered an issue while attempting to map an API request from a useState hook. The fetch operation functions correctly and returns an array of objects that I then assign to my useState variable. Subsequently, when I try to map over the useState varia ...

"Ensuring Security with Stripe Connect: Addressing Content Security Policy Challenges

Despite using meta tags to address it, the error persists and the Iframe remains non-functional. <meta http-equiv="Content-Security-Policy" content=" default-src *; style-src 'self' 'unsafe-inline'; ...

Unleashing the potential of extracting the value of a subsequent iteration while within the

Currently, I am facing a dilemma as I am unable to comprehend the logic required to design this algorithm. The problem at hand involves a sequence of images with arrows placed alternatively between each image. The structure appears as follows: Image -> ...

Iterate and combine a list of HTTP observables using Typescript

I have a collection of properties that need to be included in a larger mergeMap operation. this.customFeedsService.postNewSocialMediaFeed(this.newFeed) .mergeMap( newFeed => this.customFeedsService.postFeedProperties( newFeed.Id, this.feedP ...

What is the reason behind the angular http client's inability to detect plain text responses?

When I make a call to httpClient.get, I notice in Fiddler that both the request and response headers have text/plain format. Request: Accept: application/json, text/plain, */* Response: Content-Type: text/plain; charset=utf-8 Even though I am returning a ...

What is the best way to obtain the id of an HTML element that is generated within jQuery code?

Typically, data is retrieved in HTML by storing the html in the html file. In my case, however, I create the html element inside jQuery. So, how do I call out the div id? How can I replace document.getElementByID ("edit").innerHTML=.... when the element i ...

Tips on modifying classes within specific sections of HTML tables

I attempted to create calendar-like tables, In my script, I am trying to handle events being registered. My goal is to change the classes of cells from 2 to 5 when they are clicked on, and change the cell colors from first to hovered to aqua. For this p ...

A webpage specified with 'charset=iso-8859-1' accompanied by <!DOCTYPE HTML> is triggering a cautionary alert

After running the W3C validator on an HTML document, I discovered that using the following meta tag: <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> in conjunction with: <!DOCTYPE HTML> results in ...

What is the best way to apply a class to a button in Angular when clicking on

Creating a simple directive with two buttons. Able to detect click events on the buttons, but now looking to add a custom class upon clicking. There is a predefined class: .red { background-color: red; } The goal is to dynamically apply this class whe ...

What could be causing disparities in the padding of certain table cells compared to others?

If you're interested, I created a photo gallery over at Take a look around and feel free to download any wallpapers that catch your eye! The issue I'm encountering is with the padding of the columns - specifically, the first and last columns ha ...

Nextjs couldn't locate the requested page

After creating a new Next.js application, I haven't made any changes to the code yet. However, when I try to run "npm run dev," it shows me the message "ready started server on [::]:3000, url: http://localhost:3000." But when I attempt to access it, I ...

Ways to dynamically adjust the color of the text font?

Below is the XML content: span style="background-color: rgb(255, 255, 0); The background color mentioned here is linked to my Java code. Since this color changes dynamically, it varies in each report. My inquiry is about how to incorporate this color in ...

Pattern for identifying text that exclusively consists of whitespace characters and `<br>` tags

Here is an example of a string: <br /> <br /> <br /> Sometimes the string can look like this: <br /> <br /> Or simply like this: & ...

What is the method for bypassing libraries while troubleshooting Angular code in Visual Studio Code?

While debugging my Angular project, I keep getting into @angular/core and ts-lib which are large files with many steps. Is there a way to skip over external code during the debugging process? Below is my launch.json configuration: "version": &qu ...

Utilize the Twitter Bootstrap modal feature to play autoplaying Youtube videos that automatically pause when

Similar Question: How do I halt a video using Javascript on Youtube? I am curious about how I can utilize the modal feature in Twitter Bootstrap to automatically play a Youtube video, and when a user clicks the "close" button, it will cease the video ...

Tips for switching back and forth between two classes using jQuery?

I'm having some trouble with the toggleClass function. It doesn't seem to be working correctly for me. The image should toggle between displaying and hiding, but it only changes to hide, not back again. You can view my example here, along with t ...

Angular 7+: Trouble with displaying images from assets directory

Details regarding my Angular version: Angular CLI: 7.3.9 Node: 10.15.3 OS: win32 x64 Angular: 7.2.15 ... animations, common, compiler, compiler-cli, core, forms ... language-service, platform-browser, platform-browser-dynamic ... rout ...

Changing the container's height in an HTML document

enter image description hereim struggling to change the height of the white dash container, and I can't quite figure out how to do it. Here is the code, any help would be appreciated: analytics.html : {% extends 'frontend/base.html' %} {% l ...