Advantages of Reactive Forms over Template Driven Forms stem from the fact, controls are defined in Component programmatically and assigned to form and its inputs in Template declaratively
This makes adding controls dynamically with FormArray
, reacting to events with RxJs, Unit testing sans Template
all easier with code in component driving the logic, over html directives and /or pipes handling the same in template / dom.
However, there are aspects to form handling, in particular field validation and respective error messages that are more convenient in templates.
For example, if a field (email) is required only when field (phone) is not filled in, *ngIf
can simply remove or attr.disabled
can disable the unrequited field (email) from the DOM, and put it back as a required field based on the field (phone) value.
<form #ngForm="f" (ngSubmit)="f.valid && onSubmit(f)" novalidate> <label> Phone <input name="phone" #ngModel="phone" pattern="[0-9]{9}"> </label> <label> `*ngIf="phone.errors.pattern"> Phone Number should be digits </label>`</pre> <label> Email <input name="email" #ngModel="email"> </label> <label> `*ngIf="`phone?.value?.length || email.value?.length`"> Email is required if phone number is not given </label>` <button type="submit' [disabled]="!f.valid || (!phone.value?.length && !email.value?.length)">Submit</button></pre> </form>
In reactive form setup, having *ngIf
ain’t going to do any good. The form controls in form group, controlling form’s fields are decoupled from template DOM by design.
In a reactive form setup even If an *ngIf disables a required input in template DOM, an event must be handled in component to instrument the FormGroup’s contol declared for this input.
So how can we do declarative style validations in Reactive Forms?
Angular has handful of built in validaitors we could use in our FormGroupBuilder
to match the basic HTML 5 validators we use use in templates / DOM (required
,minLength
,maxLength
,pattern
,email
).
A special one compose
: is used when more than one validation is needed for the same form field.
A limitation here is, there is no transitive / cross field validation built-in where state of one field effects the other. We need custom group level validators, which we can build in a reusable pattern.
Checkout the full implementation by clicking [CodePen]
Let consider possible relationships between Field-1 with Field-2, there are 3 possible cases.
1. Field-1 is required only when Field-2 is given or vice-versa
2. Field-1 is not required when Field-2 is given or vice-versa
3. Either of Field-1 or Field-2 are required.
If Both fields are required, there isn’t any relationship, simply both are Validation.required at their respective field levels.
This is how we can build a reusable Custom validates for each of these cases.
class CustomValidators { static requiredWhen(requiredControlName, controlToCheckName) { return (control: AbstractControl) => { const required = control.get(requiredControlName); const toCheck = control.get(controlToCheckName); if (required.value || !toCheck.value) { removeErrors(['required'], required); return null; } const errorValue = `${requiredControlName}_Required_When_${controlToCheckName}`; setErrors({required: errorValue}, required); return {[errorValue]: true}; }; } static requiredEither(requiredControlName, controlToCheckName) { return (control) => { const required = control.get(requiredControlName); const toCheck = control.get(controlToCheckName); if (required.value || toCheck.value) { removeErrors(['required'], required); removeErrors(['required'], toCheck); return null; } const errorValue = `${requiredControlName}_Required_Either_${controlToCheckName}`; setErrors({required: errorValue}, required); setErrors({required: errorValue}, toCheck); return {[errorValue]: true}; }; } static requiredWhenNot(requiredControlName, controlToCheckName) { return (control) => { const required = control.get(requiredControlName); const toCheck = control.get(controlToCheckName); if (required.value || toCheck.value) { removeErrors(['required'], required); return null; } const errorValue = `${requiredControlName}_Required_When_Not_${controlToCheckName}`; setErrors({required: errorValue}, required); return {[errorValue]: true}; }; } } function setErrors(error: {[key: string]: any }, control: AbstractControl) { control.setErrors({...control.errors, ...error}); } function removeErrors(keys: string[], control: AbstractControl) { const remainingErrors = keys.reduce((errors, key) => { delete errors[key]; return errors; }, {...control.errors}); control.setErrors(Object.keys(remainingErrors).length > 0 ? remainingErrors : null); }
Use them declarively in your your FromBuilder group definations.
class AppComponent implements OnInit { registerForm: FormGroup; submitted = false; constructor(@Inject() private formBuilder: FormBuilder) {} ngOnInit() { this.registerForm = this.formBuilder.group({ firstName: ['', Validators.required], phone: ['', [Validators.pattern('[0-9]*')]], email: ['', [ Validators.email]] }, { validator: [ CustomValidators.requiredEither('email', 'phone') ] } ); } // convenience getter for easy access to form fields get f() { return this.registerForm.controls; } onSubmit() { this.submitted = true; // stop here if form is invalid if (this.registerForm.invalid) { return; } alert(`Submitted -> ${JSON.stringify(this.registerForm.value)}`); } }
Template just reacts to validation control
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()"> <label> Phone <input formControlName="phone"> </label> <div *ngIf="f.phone.errors" class="invalid-feedback"> <div *ngIf="f.phone.errors.required"> Phone number is required if email is not given.</div> <div *ngIf="f.phone.errors.pattern"> Phone number must match pattern digits</div> </div> <label> Email <input formControlName="email"> </label> <div *ngIf="f.email.errors"> <div *ngIf="f.email.errors.required"> Email is required ({{f.email.errors.required}}). </div> <div *ngIf="f.email.errors.email"> Email must be a valid email address </div> </div> <button [disabled]="registerForm.invalid" type="submit"> Register </button> <form>