'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