I have a Formgroup with a Formarray inside.
This is the structure:
myForm = this.fb.group(
{
title: ["", [Validators.required, Validators.minLength(4)]],
pairs: this.fb.array(
this.fPairs.map(f =>
this.fb.group({
grade: [],
value: []
})
)
)
}
);
my FormArray that gets mapped, looks like this onInit:
fPairs: Array<pairs> = [
{grade: 0, value: 0},
{grade: 0, value: 0},
{grade: 0, value: 0},
{grade: 0, value: 0}
];
What I want to achieve, as every property of each object of this FormArray is an input field in my form, I need a validation that does this:
The object properties at index 0, must have BIGGER values than the next index.
so in myForm
,
pairs[0].score > pairs[1].score
pairs[1].score > pairs[2].score
pairs[2].score > pairs[3].score
same goes for the property "value".
How can I correctly implement a real validator (type ValidatorFn
) for this formArray
?
So far I only managed to create a function, that checks for each field, compares it with the previous and the next, if the values are not following the rules, I manually set an error with setErrors()
This function is in ValueChanges()
subscription, so when a value in that formArray
changes, it checks it with my function
Is there a better way?
Here a stackblizt (the valueChanges
subscription doesn't work properly, it will refresh only when writing in the next field, you ll see what I mean in the stackblitz)
https://stackblitz.com/edit/angular-mat-formfield-flex-layout-x9nksb
thank you
So, after a while (sorry about that delay), I have made a stackblitz reproducing your minimal example, and I made a validator for you. The code is at the end of my answer.
To explain it briefly to you : cross-control validation must be made in the parent. The parent can be either a form group or a form array. In your case, that would be a form array (the array that contains all of your scores).
The errors are then appended directly to your form array, rendering it invalid when the condition is met.
As you can see, the controls don't have any idea of their errors : I voluntarily did that, so that I don't work for you. The endgoal for me was to show you how to compare two fields and set form errors accordingly.
Now, in the validator, you can add/remove errors from your controls if you want, but I think I have answered your first question about form validation on two separate fields.
The code is self explained, but if you have questions, feel free to ask them !
(PS : the errors are shown in the footer of the stackblitz page if you want to see them)
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, ValidatorFn } from '@angular/forms';
@Component({
selector: 'my-app',
template: `
<form novalidate [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<div formArrayName="pairs" fxLayout="row" fxLayoutGap="12px" *ngFor="let pair of form.get('pairs').controls; let i = index">
<ng-container [formGroupName]="i">
<mat-form-field fxFlex="50%">
<input matInput type="text" formControlName="grade" placeholder="Grade for {{ i }}">
</mat-form-field>
<mat-form-field fxFlex="50%">
<input matInput type="text" formControlName="value" placeholder="Score for {{ i }}">
</mat-form-field>
</ng-container>
</div>
</form>
<p>The form is {{ form.invalid ? 'invalid' : 'valid' }}</p>
<p>The pairs group is {{ form.get('pairs').invalid ? 'invalid' : 'valid' }}</p>
<p>Errors on the form : {{ form.errors | json }}</p>
<p>Errors on the group : {{ form.get('pairs').errors | json }}</p>
`,
styleUrls: ['./app.component.css']
})
export class AppComponent {
form: FormGroup;
// Create a dataset
data = [
{ grade: 6, value: 0 },
{ grade: 5, value: 0 },
{ grade: 4, value: 0 },
{ grade: 3, value: 0 },
];
constructor(private fb: FormBuilder) { }
ngOnInit(): void {
// Create a form group
this.form = this.fb.group({
// Create a form array made of form groups
pairs: this.fb.array(this.data.map(item => this.fb.group(item)))
});
// Add validators (optional, used to split the code logic)
this.addValidators();
}
addValidators() {
// Get the form array and append a validator (again code split)
(this.form.get('pairs') as FormArray).setValidators(this.formArrayValidator());
}
// Form validator
formArrayValidator(): ValidatorFn {
// The validator is on the array, so the AbstractControl is of type FormArray
return (group: FormArray) => {
// Create an object of errors to return
const errors = {};
// Get the list of controls in the array (which are FormGroups)
const controls = group.controls;
// Iterate over them
for (let i = 1; i < controls.length; i++) {
// Get references to controls to compare them (split code again)
const valueControl = controls[i].get('value');
const previousValueControl = controls[i - 1].get('value');
// if error, set array error
if (valueControl.value > previousValueControl.value) {
// array error (sum up of all errors)
errors[i + 'greaterThan' + (i - 1)] = true;
}
}
// return array errors ({} is considered an error so return null if it is the case)
return errors === {} ? null : errors;
}
}
}