Require one from two fields using Angular 2

I created a custom validator directive:

import {
    FormGroup,
    ValidationErrors,
    ValidatorFn,
    Validators,
  } from '@angular/forms';

  export const atLeastOne = (validator: ValidatorFn, controls:string[] = null) => (
    group: FormGroup,
  ): ValidationErrors | null => {
    if(!controls){
      controls = Object.keys(group.controls)
    }

    const hasAtLeastOne = group && group.controls && controls
      .some(k => !validator(group.controls[k]));

    return hasAtLeastOne ? null : {
      atLeastOne: true,
    };
  };

To use it, you just do this:

this.form = this.formBuilder.group({
            name: ['', Validators.required],
            email:[''],
            telefone:[''],
            message:['', Validators.required],
        }, { validator: atLeastOne(Validators.required, ['email','telefone']) });

So email or telefone would be required here. If you leave it empty then any control with a value is fine and you can use it with any type of validator, not just Validators.required.

This is reusable in any form.


I have updated my validator snippet to support string and number types

I was inspired by Todd Skelton. This is a very simple Validator that does just what you ask for and nothing else:

/**
 * Validates if at least one of the provided fields has a value.
 * Fields can only be of type number or string.
 * @param fields name of the form fields that should be checked
 */
export function atLeastOne(...fields: string[]) {
  return (fg: FormGroup): ValidationErrors | null => {
    return fields.some(fieldName => {
      const field = fg.get(fieldName).value;
      if (typeof field === 'number') return field && field >= 0 ? true : false;
      if (typeof field === 'string') return field && field.length > 0 ? true : false;
    })
      ? null
      : ({ atLeastOne: 'At least one field has to be provided.' } as ValidationErrors);
  };
}

And here is how I use it:

  ngOnInit(): void {
    this.form = this.formBuilder.group(
      {
        field: [this.field],
        anotherField: [this.anotherField],
      },
      { validator: atLeastOne('field','anotherField') },
    );
  }

Yes, a custom validator is the way to go.

Make your form group like this:

this.contact = this.formBuilder.group({
  name: ['', Validators.required],
  email: ['', Validators.required],
  phone: ['', Validators.required],
  message: ['', Validators.required]
}, {validator: this.customValidationFunction})

Then have the customValidationFunction check for validation. Made up validation just for example:

customValidationFunction(formGroup): any {
   let nameField = formGroup.controls['name'].value; //access any of your form fields like this
   return (nameField.length < 5) ? { nameLengthFive: true } : null;
}

Change each input like this (changing your p tags to divs. Substitute the control name for each and change syntax for the hidden span tag validation where appropriate):

<div [ngClass]="{'has-error':!contact.controls['name'].valid && contact.controls['name'].touched}">
    <label>Name</label>
    <input class="input" type="text" [formControl]="contact.controls['name']">
    <span [hidden]="!contact.hasError('nameLengthFive')" class="error">Enter your name</span>
</div>

Slightly refactored Florian's answer for the lazy as it was making ts-sonar angry (cognitive-complexity rule):

const isFieldEmpty = (fieldName: string, fg: FormGroup) => {
    const field = fg.get(fieldName).value;
    if (typeof field === 'number') { return field && field >= 0 ? true : false; }
    if (typeof field === 'string') { return field && field.length > 0 ? true : false; }
};

export function atLeastOne(...fields: string[]) {
  return (fg: FormGroup): ValidationErrors | null => {
    return fields.some(fieldName => isFieldEmpty(fieldName, fg))
      ? null
      : ({ atLeastOne: 'At least one field has to be provided.' } as ValidationErrors);
  };
}