Angular Form Validation: Ensuring Data Accuracy

Utilizing angular reactive form to create distance input fields with two boxes labeled as From and To.

HTML:

<form [formGroup]="form">
  <button (click)="addRow()">Add</button>
  <div formArrayName="distance">
    <div
      *ngFor="let item of form.get('distance').controls; let i = index"
      [formGroupName]="i"
      style="display: flex"
    >
      <input type="number" placeholder="From" formControlName="from" />
      <div><input type="number" placeholder="To" formControlName="to" /></div>
    </div>
  </div>
  <br /><br />
  <button type="submit" [disabled]="!form.valid">Submit</button>
</form>

TypeScript:

ngOnInit() {
  this.form = this.fb.group({
    distance: this.fb.array([]),
  });
  this.addRow()
}

addRow() {
  const control = this.form.controls.distance as FormArray;
  control.push(this.fb.group({
    from: ['',Validators.required],
    to: ['',Validators.required]
  }));
}

The default view displays two input boxes for from and to.

An 'add' button is present at the top. Clicking it adds rows with similar input fields arranged as an array.

The requirement is to prevent users from entering a previous row's to value or any lesser value in the current row's from input box.

Example scenario - In the first row, if user enters values like 0 and 5 for from and to respectively,

  "distance": [
    {
      "from": 0,
      "to": 5
    }
  ]

In the next row, when adding values to the from input box, any value equal to or less than 5 (which has already been entered) is considered invalid.

Therefore, the following configuration is incorrect:

{
  "distance": [
    {
      "from": 0,
      "to": 5
    },
    {
      "from": 5,
      "to": 10
    }
  ]
}

Values such as "from": 5,, "from": 4(or)3(or)2(or)1, are not valid in the second row.

Only values equal to or greater than 6 are acceptable.

This validation needs to be applied for each row by comparing with the previous row's to value.

Assistance is requested on implementing this type of validation to restrict users from entering a previous row's to value or a lesser value in the current row's from field.

Working Example: https://stackblitz.com/edit/disable-group-control-value-on-another-control-value-for-j58atx

Edit:

Attempted using input change event listener like below,

<input type="number" (input)="onInputChange($event.target.value)" placeholder="From" formControlName="from">

Refer to the link https://stackblitz.com/edit/disable-group-control-value-on-another-control-value-for-ymfpkj for details, seeking confirmation on correctness of approach.

Please advise on any necessary changes to the procedure if deemed incorrect.

Answer №1

After considering my options, I made the decision to split the two conditions. You can view the updated code on this new stackblitz

  ngOnInit() {
    this.form = this.fb.group({
      distance: this.fb.array([], this.distanceValidator()),
    });
    this.addRow()
  }

  addRow() {
    const control = this.form.controls.distance as FormArray;
    control.push(this.fb.group({
      from: ['', Validators.required],
      to: ['', Validators.required]
    }, { validator: this.greaterValidator() }));
  }
  setDefault() {
    const control = this.form.controls.distance as FormArray;
    this.default.forEach(data => {
      control.push(this.fb.group({
        from: [data.from, Validators.required],
        to: [data.to, Validators.required]
      }, { validator: this.greaterValidator() }));
    });
  }
  greaterValidator() {
    return (fa: FormGroup) => {
      return fa.value.to && fa.value.to < fa.value.from ? { error: "from greater than to" } : null;
    }
  }
  distanceValidator() {
    return (fa: FormArray) => {
      let ok = true;
      for (let i = 1; i < fa.value.length; i++) {
        ok = (!fa.value[i].from || fa.value[i].from > fa.value[i - 1].to) && (!fa.value[i].to || fa.value[i].to > fa.value[i - 1].from);
        if (!ok)
          return { error: "from/to yet included", index: i }
      }
      return null
    }
  }

And here is the corresponding .html code:

<form [formGroup]="form">
    <button (click)="addRow()">Add</button>
  <div formArrayName="distance" >
    <div 
      *ngFor="let item of form.get('distance').controls; let i = index" 
      [formGroupName]="i" 
      style="display: flex">
      <input type="number" 
        placeholder="From" 
        formControlName="from">
      <div>
        <input type="number"
          placeholder="To" 
          formControlName="to">
      </div>
      <span *ngIf="item.errors">*</span>
      <span *ngIf="form.get('distance')?.errors && form.get('distance')?.errors.index==i">**</span>
    </div>
  </div>
  <div *ngIf="form.get('distance')?.errors">{{form.get('distance')?.errors.error}}</div>
  <br><br>
  <button type="submit" [disabled]="!form.valid"> Submit </button>
</form>
<button (click)="setDefault()"> Set Default Values </button>

Update: I have decided that errors will only be displayed when they are found and not beyond. Additionally, if the fields before 'from' and 'to' are empty, no error should be given. To achieve this, we can convert them to numbers by adding a '+' sign like so:

let ok = (!fa.value[i].from || fa.value[i].from > +fa.value[i - 1].to)
        && (!fa.value[i].to || fa.value[i].to > +fa.value[i - 1].from);

(notice the "+" in +fa.value[i-1].to and +fa.value[i-1].from)

In order to display errors more effectively, imagine that in a scenario with 6 rows, lines at position 0, 3, and 4 (0 being the first row) report an error like this:

{error:"there are errors",indexError:",0,3,4,"}

This allows us to include conditional statements within the *ngFor loop, such as:

  <span *ngIf="form.get('distance')?.errors && 
      form.get('distance')?.errors.indexError.indexOf(','+i+',')>=0">
       **
  </span>

Therefore, our revised distanceValidator function looks like this:

  distanceValidator() {
    return (fa: FormArray) => {
      let indexError:string="";
      for (let i = 1; i < fa.value.length; i++) {
        let ok = (!fa.value[i].from || fa.value[i].from > +fa.value[i - 1].to) && (!fa.value[i].to || fa.value[i].to > +fa.value[i - 1].from);
        if (!ok)
          indexError+=','+i;
      }
      return indexError?{error:"there are errors",indexError:indexError+','}:null
    }

Some may argue that returning an array of errors would be better, but it complicates the process of identifying specific rows with errors using interpolation. Hence, the approach mentioned above. Also, while it's possible to check all preceding rows for errors, I believe keeping the code concise is crucial. Remember, the distanceValidator function is executed multiple times.

To explore further examples, you can visit another updated stackblitz link

Answer №2

To implement custom validation, I opted to use the validation within the same component.

ngOnInit() {
    this.form = this.fb.group({
      distance: this.fb.array([], this.distanceValidator()),
    });
    this.addRow()
  }
  distanceValidator() {
    return (fa: FormArray) => {
      let isValid = true;
      let isValid2 = fa.value.length ? (!fa.value[0].to || !fa.value[0].from) || fa.value[0].to > fa.value[0].from : true;
      if (!isValid2)
        return { error: "From value is greater than To value" }
      for (let i = 1; i < fa.value.length; i++) {
        if (fa.value[i].from && fa.value[i].to )
        {
        isValid = (fa.value[i].from > fa.value[i - 1].to || fa.value[i].to < fa.value[i - 1].from);
        isValid2 = (fa.value[i].to > fa.value[i].from);
        if (!isValid)
          return { error: "From/To values are overlapping" }
        if (!isValid2)
          return { error: "From value is greater than To value" }
        }
      }
      return isValid && isValid2 ? null : !isValid?{ error: "From value is overlapping" }:{ error: "From value is greater than To value" }
    }
  }

You can display the error message as follows:

 <div *ngIf="form.get('distance')?.errors">
     {{form.get('distance')?.errors.error}}
 </div>

For more details, check out the [forked stackblitz example][1]

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

Creating a custom design for ng-bootstrap accordion using CSS styling

I've encountered an issue with my Angular 2 component that utilizes an accordion from ng-bootstrap. While the functionality works perfectly, I'm facing a problem with applying custom styles using the .card, .card-header, and .card-block classes o ...

Managing JavaScript expiration time in API responses

Seeking help with a JavaScript API I'm new to. The response I received contains a timestamp, which seems like it's in milliseconds. I want to format this time for a countdown but I'm not sure what these milliseconds are referring to. I know ...

Issue with Vue.js 2.0 transition not triggering on changing routes dynamically

I've encountered an issue where transitions are not firing on dynamic routes with parameters. For example, when navigating from /chapter/1 to /chapter/2, no transition occurs. However, when going from /chapter/1 to /profile/1, a transition does occur! ...

Changing the li tag by clicking on it is a simple task that can be easily

Whenever I click on a tag, I want the li class to change to "active" and open a new page with the corresponding tag as active. For example, if I click on the Overview tag, the new page should open with the li tag as active. I have attempted to write some c ...

Angular2 and the Use of Environment Variables

I am working on an angular 2/4 app with a node server.js to serve it. I need to access an environment variable for switching between backend endpoints (using localhost for dev and another endpoint for prod). Both the frontend and backend apps are destined ...

Having trouble locating the name WebGLObject in my TypeScript code

Every time I try to run ng serve command An error pops up on my screen saying: "WebGLObject cannot be found." ...

Encountering an issue with undefined property 'path' while attempting to upload an image on the frontend in React

I have encountered an issue while trying to upload an image on the frontend. The error message displayed is: message: "Cannot read property 'path' of undefined" status: "fail" When I log req.file on the backend and attempt to ...

Allow editing for only a specific part of a text box

Creating a customized personal URL page for users on my site is important to me. I envision the value of a text box being "example.com/username," with the "example.com/" part displayed in the text box, but not editable. I've noticed that Tumblr accom ...

Utilizing the `theme` property in makeStyles with Material-UI

I am currently working on passing down an muiTheme to a component through the use of ThemeProvider, and I also want to apply it to the children components. Furthermore, I aim to utilize the theme's properties in both components by creating a classes o ...

What is the correct method for importing a Node module into Angular using TypeScript or AngularCLI?

As I integrate some "legacy" (non-typescript) JavaScript libraries into my Angular single page application. Usually, I simply include a CDN link in the index.html file like this: <script src="//cdnjs.cloudflare.com/ajax/libs/pako/1.0.6/pako.min.js"> ...

Creating a clickable color-changing grid: A step-by-step guide

In a grid of size 12 by 12, each corner will be clickable, selecting a 6x3 cell area starting from the corner. The selected cells will change color upon clicking any of the 4 corners. After selecting one corner, the remaining cells (126 cells) will be che ...

Integrate CKEditor with elFinder to allow for direct file uploads

I am utilizing the elFinder Laravel package for file management with CKEditor. I have followed all the steps and everything is working fine except for one issue. When I click on the image button in CKEditor to select or upload an image, after selecting an ...

Exploring the Comparison Between Angular RxJS Observables: Using takeUntil and unsubscribing via Subscription

When it comes to unsubscribing from observables in Angular components utilizing ngOnDestroy, there are multiple approaches available. Which of the following options is more preferable and why? Option 1: takeUntil The usage of RxJS takeUntil for unsubscri ...

The search results fail to show the required information

I am trying to retrieve data from an API based on a search query. When the user enters the name of the film they are looking for and hits enter to submit the form, the matching films should be displayed on the screen. However, my console is showing errors ...

What are some ways to enhance the opacity of a Material UI backdrop?

I'm looking to enhance the darkness of the material UI backdrop as its default appearance is not very dark. My goal is to make it dimmer and closer to black in color. ...

Guide to authenticating with npm using various user accounts for different scopes within the same registry

I am facing an issue with setting up multiple npm authTokens for two different scopes on the same registry. @scope1:registry=https://registry.npmjs.org/ @scope2:registry=https://registry.npmjs.org/ //registry.npmjs.org/:_authToken=${NPM_TOKEN} I have atte ...

The 'GoogleAuthProvider' property cannot be found on the 'AngularFireAuth' type

When attempting to sign in with Google using 'AngularFireAuth', I encountered an error. Here is the code snippet from my auth.service.ts file: import { Injectable } from '@angular/core'; import { first } from 'rxjs/operators'; ...

What is the method for determining the length of a piped array within a template?

Here is a sample code snippet showcasing how to get the length of an array when not using a pipe: <my-component [arrLen]='arrayA.length'></my-component> But what if you want to get the length of a filtered array using a pipe? The exa ...

What could be causing the server to return an empty response to an ajax HTTP POST request?

Attempting to make a POST request using ajax in the following manner: $.ajax({ type: "POST", url: 'http://192.168.1.140/', data: "{}", dataType: "json", ...

Creating a Custom Alert Box in Javascript with Image Alignment

I have just finished creating a custom alert box using Javascript, complete with text and images. However, I am facing an issue with alignment. The output is not as expected. I am trying to display the correct mark and text on the same line. Can someone ...