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

Form popup that closes without refreshing the page upon submission

My goal is to make this form close when the button is pressed without refreshing the page, as Ajax code will be added later. The form functions as a pop-up, so it needs to close once the button is clicked. Currently, I can click the button without a refres ...

Mastering the Art of Manipulating Z-Index Stacking Context in CSS Using Transforms

I'm struggling to position the red square on top of the table. After reading articles about Z-index Stacking Context and browsing through this stack overflow page about overriding CSS Z-Index Stacking Context, I still can't seem to figure it out. ...

Creating a Node server exclusively for handling POST requests is a straightforward process

I'm looking to set up a Node server that specifically handles POST requests. The goal is to take the data from the request body and use it to make a system call. However, my current setup only includes: var express = require('express'); var ...

Eliminating unnecessary gaps caused by CSS float properties

I need help figuring out how to eliminate the extra space above 'Smart Filter' in the div id='container_sidebar'. You can view an example of this issue on fiddle http://jsfiddle.net/XHPtc/ It seems that if I remove the float: right pro ...

Incorporate dynamic HTML into Angular by adding CSS styling

Just starting out with Angular and have limited front-end experience. I'm feeling a bit lost on how to proceed. Currently, I have a mat-table with a static datasource (will connect to a database in the future), and I need to dynamically change the bac ...

The process of querying two MySQL tables simultaneously in a Node.js environment using Express

My goal is to display both the article and comments when a user clicks on a post. However, I've encountered an issue where only the post loads without the accompanying comments. Here is the code snippet that I've been working with: router.get(&a ...

Utilize the Multer file upload feature by integrating it into its own dedicated controller function

In my Express application, I decided to keep my routes.js file organized by creating a separate UploadController. Here's what it looks like: // UploadController.js const multer = require('multer') const storage = multer.diskStorage({ dest ...

various positions for ngb properties

My input field has both a tooltip and a dropdown attached to it using the ngb attributes: <input placement="right" ngbTooltip="Search" [ngbTypeahead]="search" /> The issue I'm facing is that I want the tooltip to appear on the right ...

When applying the OWASP ESAPI encodeForHTMLAttribute method, I noticed that symbols are being rendered as their corresponding HTML entity numbers instead of the actual symbols

I recently started exploring OWASP ESAPI for preventing XSS and integrating the JavaScript version into my application. As per Rule #2 in the XSS prevention cheat sheet, it is recommended to "Attribute Escape" before inserting untrusted data into attribut ...

Ensure that the element is positioned above other elements, yet below the clickable area

Currently, I am working on developing a notification system that will display a small box in the top right corner of the screen. However, I am facing an issue where the area underneath the notification is not clickable. Is there a way to make this part o ...

What is the most effective way to incorporate the DOMContentloaded event listener into the document using nextJS?

I am working on integrating an HTML code snippet into my Next.js project. The snippet includes an external script with a createButton function. <div id="examplebtn"></div> <script type="text/javascript"> //<![ ...

The reduce function is displaying an undefined result

Check out this code snippet: const filterByType = (target , ...element) => { return element.reduce((start, next) =>{ if(typeof next === target){ start.push(next) } } , []) } I'm trying to achieve a specific g ...

Attempting to conceal an HTML 'label' element by employing CSS 'hide' properties

Why is it so difficult to hide a checkbox that says "Remember Me" on my membership site login form? The HTML code snippet causing the issue: <label><input name="rememberme" type="checkbox" id="rememberme" value="forever" /> Remember Me</la ...

What is the best way to display two radio buttons side by side in HTML?

Looking at my HTML form in this JSFiddle link, you'll see that when the PROCESS button is clicked, a form with two radio buttons appears. Currently, they are displayed vertically, with the female radio button appearing below the male radio button. I& ...

What are some strategies to reduce the frequency of API calls even when a webpage is reloaded?

Imagine I'm utilizing a complimentary API service that has a restriction of c calls per m minutes. My website consists of a simple HTML file with some JavaScript code like the one below: $(function () { //some code function getSomething() { ...

Incorporating CSS and JS files into a WordPress theme

To incorporate Css & Js files into my website pages, I plan to insert the following code into the functions.php file: function cssjsloading(){ wp_enqueue_style('bootstrap-rtl', get_template_directory_uri() . '/css/bootstrap-rtl.css&apo ...

When nodemon is executed, it encounters an "Error: Cannot find module" issue, indicating that it may be searching in the incorrect directory

I recently encountered an issue with my Node.js project that utilizes nodemon. Despite having the package installed (located in /node_modules), I am experiencing an error when trying to start my Express server with nodemon through a script in my package.js ...

Unleashing the Power of Vuejs: Setting Values for Select Options

I have a specific requirement in my Vue application where I need to extract values from an XLS file and assign them to options within a select dropdown. Here is what I currently have: <tr v-for="header in fileHeaders" :key="header"&g ...

Creating JSX elements in React by mapping over an object's properties

I need to display the properties of an object named tour in JSX elements using React without repeating code. Each property should be shown within a div, with the property name as a label and its value next to it. Although I attempted to iterate over the o ...

Retrieving data from MongoDB for rendering on an HTML page

I have successfully inserted data into my MongoDB database, but I am facing issues with the "get" function to display this data on my HTML page. My current setup involves using Node.js with Express framework and Angular for front-end development and routi ...