30

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.

Doug Domeny
  • 4,410
  • 2
  • 33
  • 49
Manny
  • 1,034
  • 1
  • 11
  • 16
  • 1
    You need to specify a validator on the group itself, and this validator needs to check that at least one of its controls has a value. – JB Nizet Oct 29 '16 at 16:27
  • This [**tutorial**](https://scotch.io/tutorials/how-to-implement-conditional-validation-in-angular-2-model-driven-forms) might be useful. – developer033 Oct 29 '16 at 17:59
  • Thank you JB Nizet for the hint. – Manny Oct 31 '16 at 13:35

5 Answers5

47

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) })
}
Merott
  • 7,189
  • 6
  • 40
  • 52
10

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);
  }
}
angryip
  • 2,140
  • 5
  • 33
  • 67
AnTiToinE
  • 421
  • 5
  • 16
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,
    };
  }
}
Sandeep Maharjan
  • 124
  • 3
  • 15
0

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;
  };
};
dhilt
  • 18,707
  • 8
  • 70
  • 85
0

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));
}
SKL
  • 1,243
  • 4
  • 32
  • 53