'Angular2 FormBuilder Validators: require at least one field in a group to be filled
I have a form where I'm collecting phone numbers (mobile, personal, other). I need to have at least input populated. I'm trying to use Angular2 FormBuilder.
After much research I'm having a problem getting my head around this problem. I know I can do it using other methods but I was wondering if it's possible using FormBuilder Validators. If I add "Validators.required" then all 3 fields are required. Any suggestions or ideas?
phone: this._fb.group(
{
other: [''],
personal: [''],
mobile: [''],
}
Base on the hint from " JB Nizet", here's what I had to implement to make it work:
My group Validator (it still needs tweaking):
static phoneExists(group: FormGroup): { [key: string]: any } {
if (null != group) {
var other: AbstractControl = group.controls['other'];
var mobile: AbstractControl = group.controls['mobile'];
var personal: AbstractControl = group.controls['personal'];
var data: Object = group.value;
return (
(other.valid && isEmptyInputValue(other.value))
&& (mobile.valid && isEmptyInputValue(mobile.value))
&& (personal.valid && isEmptyInputValue(personal.value))
)
? { 'required': true }
: null;
}
}
My group change:
phone: this._fb.group(
{
other: [''],
personal: [''],
mobile: [''],
},
{ validator: MyValidators.phoneExists }
)
It took me a while, but the key is to add the key word "validator" and it will cause the group validator to fire.
In the HTML i added the following:
<small *ngIf="!myForm.controls.profile.controls.phone.valid" class="text-danger">
At least one phone is required.
</small>
I hope this help anyone else.
Solution 1:[1]
I use an atLeastOne function that creates a custom validator based on any existing validator:
import { FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
export const atLeastOne = (validator: ValidatorFn) => (
group: FormGroup,
): ValidationErrors | null => {
const hasAtLeastOne =
group &&
group.controls &&
Object.keys(group.controls).some(k => !validator(group.controls[k]));
return hasAtLeastOne ? null : { atLeastOne: true };
};
The beauty is that you can use any validator with it and not just Validators.required.
In OP's case, it'll be used like this:
{
phone: this._fb.group({
other: [''],
personal: [''],
mobile: [''],
}, { validator: atLeastOne(Validators.required) })
}
Solution 2:[2]
This is a generic code that you can use with every FormGroup:
export function AtLeastOneFieldValidator(group: FormGroup): {[key: string]: any} {
let isAtLeastOne = false;
if (group && group.controls) {
for (const control in group.controls) {
if (group.controls.hasOwnProperty(control) && group.controls[control].valid && group.controls[control].value) {
isAtLeastOne = true;
break;
}
}
}
return isAtLeastOne ? null : { 'required': true };
}
And the usage:
@Component({
selector: 'app-customers',
templateUrl: './customers.component.html',
styleUrls: ['./customers.component.scss']
})
export class CustomersComponent implements OnInit {
public searchCustomerForm: FormGroup;
constructor() { }
ngOnInit() {
this.searchCustomerForm = new FormGroup({
customerID: new FormControl(''),
customerEmail: new FormControl(''),
customerFirstName: new FormControl(''),
customerLastName: new FormControl('')
}, AtLeastOneFieldValidator);
}
}
Solution 3:[3]
Accepted answer is correct however, you will get depreciated warning in latest angular versions. so, for newer version try this:
employee: this.fb.group({
FirstName: [null],
LastName: [null],
Dob: [null],
}, { validators: atLeastOne(Validators.required, ["FirstName", "LastName"]) }),
Validation:
export function atLeastOne(validator: ValidatorFn, controls: string[] = []): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control) return null;
const formGroup = control as FormGroup;
return (formGroup && controls.some(k => !validator(formGroup.controls[k]))) ? null : {
atLeastOne: true,
};
}
}
Solution 4:[4]
Here's the approach for Forms with no sub groups. It allows to have a field validator, not a group one.
const atLeastOneList = ['field2', 'field4', 'field5'];
this.form = this.fb.group({
field1: [''],
field2: ['', this.requiredAtLeastOne(atLeastOneList)],
field3: [''],
field4: ['', this.requiredAtLeastOne(atLeastOneList)],
field5: ['', this.requiredAtLeastOne(atLeastOneList)],
});
The method implementation should contain an implicit protection from the "Maximum call stack size exceeded" error, because we are going to re-validate the fields and we need to avoid recursion.
requiredAtLeastOne(fields: string[]) {
return (control: FormControl) => {
// check if at least one field is set
const result = fields.some(name => {
const ctrl = control.parent.get(name);
return ctrl && ctrl.value && ctrl.valid;
});
// run at-least-one validator for other fields
Object.entries(control.parent.controls)
.filter(([name, ctrl]) =>
// here we are, proper filter prevents stack overflow issue
fields.includes(name) && ctrl !== control && !ctrl.valid && result
)
.forEach(([, ctrl]) => ctrl.updateValueAndValidity())
return !result ? { requiredAtLeastOne: true } : null;
};
};
Solution 5:[5]
Just add on @Merott answer. Latest angular version throw FormBuilder group is deprecated message. You simply can use angular setValidators to update it dynamically
ngOnInit(): void {
this.form.setValidators(atLeastOne(Validators.required));
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | angryip |
| Solution 3 | Sandeep Maharjan |
| Solution 4 | dhilt |
| Solution 5 | SKL |
