Angular Reactive Forms Advanced
Advanced reactive forms model complex data with FormGroup/FormArray, combine sync/async validators, and update efficiently with patchValue and updateOn; observe valueChanges/statusChanges for reactive logic.
Reactive Forms Advanced Essentials
- Structure: Use
FormGroupandFormArrayto model complex forms. - Validation: Combine sync and async validators at control and group levels.
- Updates: Use
patchValuefor partial updates;setValuerequires the full shape.
import { FormBuilder, Validators, FormArray } from '@angular/forms';
fb.group({
name: ['', Validators.required],
tags: fb.array([ fb.group({ label: ['Angular'] }) ])
});
// Add row
(form.get('tags') as FormArray).push(fb.group({ label: [''] }));
Example explained
- fb.group({...}): Creates a
FormGroupwith controls and validators. - FormArray: Holds an ordered list of controls/groups for dynamic rows.
- Push a row:
(form.get('tags') as FormArray).push(fb.group({ label: [''] }))adds a new group.
Notes:
- Related: See Forms and Services & DI.
- Keep form state in a service for reuse across routes.
- setValue vs patchValue:
setValuerequires the full shape;patchValueupdates a subset. - Validate at the control and group levels.
Nested Groups and Arrays
Group related controls for structure and reuse.
Use FormArray for dynamic lists like tags or items. A FormArray is an ordered list of controls whose length can change at runtime.
const tags = fb.array([ fb.group({ label: ['Angular'] }) ]);
tags.push(fb.group({ label: [''] }));
Example explained
- fb.array([...]): Initializes a
FormArraywith oneFormGrouprow. - Add row:
tags.push(fb.group({ label: [''] }))appends another tag group.
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators, FormArray } from '@angular/forms';
import { JsonPipe, CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, JsonPipe],
template: `
<h3>Advanced Reactive Form</h3>
<form [formGroup]="form" (ngSubmit)="submit()">
<input placeholder="Name" formControlName="name">
<div formArrayName="tags">
<div *ngFor="let t of tags.controls; let i = index" [formGroupName]="i">
<input placeholder="Tag" formControlName="label">
</div>
</div>
<button type="button" (click)="addTag()">Add Tag</button>
<button type="submit">Submit</button>
</form>
<pre>{{ form.value | json }}</pre>
`
})
class App {
fb = new FormBuilder();
form = this.fb.group({
name: ['', Validators.required],
tags: this.fb.array([this.fb.group({ label: ['Angular'] })])
});
get tags(): FormArray { return this.form.get('tags') as FormArray; }
addTag() { this.tags.push(this.fb.group({ label: [''] })); }
submit() { alert(JSON.stringify(this.form.value)); }
}
bootstrapApplication(App);
<app-root></app-root>
Example explained
- [formGroup]: Binds the form model to the template.
- formArrayName: Points to the
tagsarray; each row uses[formGroupName]="i". - Getter + push: The
tagsgetter returns theFormArray;addTag()pushes a new group. - Submit: Reads
form.value, which reflects the nested group/array structure.
Binding tips: Use formArrayName and [formGroupName] for each row to keep bindings aligned.
Consistent shapes: Push groups with the same control shape to a FormArray; avoid mixing primitives and groups.
Dynamic lists: When rendering with *ngFor, use trackBy to keep inputs stable while adding/removing rows.
Validation Strategies
- Use synchronous validators for most rules; they are fast and simple.
- Use async validators for server checks/uniqueness; they run after sync validators.
- Keep validation lean and debounce inputs before async checks.
import { AbstractControl, ValidationErrors } from '@angular/forms';
function banned(value: string[]): (c: AbstractControl): ValidationErrors | null {
return (c) => value.includes(c.value) ? { banned: true } : null;
}
fb.control('', [Validators.required, banned(['admin'])]);
Example explained
- Validator factory:
banned([...])returns a function(c: AbstractControl) => ValidationErrors | null. - Control-level: Attach sync validators like
Validators.requiredand custom rules to a control.
// Group-level validator and updateOn: 'blur'
import { AbstractControl, ValidationErrors } from '@angular/forms';
function samePassword(group: AbstractControl): ValidationErrors | null {
const pass = group.get('pass')?.value;
const confirm = group.get('confirm')?.value;
return pass === confirm ? null : { mismatch: true };
}
const form = fb.group(
{ pass: [''], confirm: [''] },
{ validators: samePassword, updateOn: 'blur' }
);
Guidelines:
- Use group-level validators for cross-field rules (e.g., password match).
- Reduce churn with
updateOn: 'blur' | 'submit'(delay validation/value changes until that event) when appropriate. - Show errors based on
touched/dirtyto avoid noisy UX. - Async validators should be fast and cancelable; debounce inputs before triggering server checks.