This (minimal demo) code obviously works, but I am a bit taken aback by the hoops I needed to jump through (and little to no mention in Angular (v13) documentation about the technique). I'm still inexperienced with Angular in this regard...
I need to validate a form (Angular reactive form pattern) on both the field level and the form level:
- Field A (if supplied) needs to be exactly 3 or 4 characters long (field validation
validateLength()
); - At least one of Field A and Field B needs to be supplied (form validation
validateOneOf()
, but display error messages at the level of the applicable fields); - The submit button should be disabled while the form is not valid.
I have found that in the form validator, I need to set errors for each field, BUT also clear errors for this validator if it is valid, WITHOUT removing any other errors. So is there any cleaner way to write validateOneOf()
?
Template
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div>
<label>A</label><input type="text" formControlName="fieldA"/>
<span *ngIf="form.get('fieldA').touched && form.get('fieldA').errors" class="errMsg">
<span *ngIf="form.get('fieldA').errors.provideOneOf">Provide at least one of the values</span>
<span *ngIf="form.get('fieldA').errors.length">Wrong length</span>
</span>
</div>
<div>
<label>B</label><input type="text" formControlName="fieldB"/>
<span *ngIf="form.get('fieldB').touched && form.get('fieldB').errors" class="errMsg">
<span *ngIf="form.get('fieldB').errors.provideOneOf">Provide at least one of the values</span>
</span>
</div>
<div>
<button type="submit" [disabled]="!form.valid">Submit</button>
</div>
</form>
Component
import { Component } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
@Component({
selector: 'app-test-multi-field',
templateUrl: './test-multi-field.component.html',
styleUrls: ['./test-multi-field.component.css'],
})
export class TestMultiFieldComponent {
form = this.formBuilder.group(
{
fieldA: ['', TestMultiFieldComponent.validateLength([3, 4])],
fieldB: [''],
},
{ validators: [TestMultiFieldComponent.validateOneOf(['fieldA', 'fieldB'])] }
);
constructor(private formBuilder: FormBuilder) {}
static validateLength(lengths: number[]): ValidatorFn {
return (control: FormControl): ValidationErrors | null => {
const isValid = !control.value || lengths.some((len) => len === control.value?.length);
return isValid ? null : { length: true };
};
}
static validateOneOf(controls: string[]): ValidatorFn {
const errKey = 'provideOneOf';
const err = { [errKey]: true };
return (form: FormGroup): ValidationErrors => {
const isValid = controls.some((name) => form.get(name).value);
controls.forEach((name) => {
const errors = form.get(name).errors || {};
if (isValid) {
delete errors[errKey];
form.get(name).setErrors(Object.keys(errors).length ? errors : null);
} else {
form.get(name).setErrors({ ...errors, ...err });
}
});
return isValid ? null : err;
};
}
onSubmit(): void {
if (this.form.valid) {
console.log('submitting presumed valid form');
} else {
console.log('tried to submit invalid form');
}
}
}