FormArray manages a dynamic list of form controls — adding and removing controls at runtime in response to user actions. The post form’s tags are a classic FormArray use case: the user starts with one tag input and can add more or remove any. Unlike a fixed FormGroup, FormArray’s length is not known at form creation time. Angular 14+’s typed forms give FormArray a typed parameter: FormArray<FormControl<string>> means each element is a FormControl of type string.
FormArray — Dynamic Tags
import { FormBuilder, FormArray, FormControl,
Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-post-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- Static fields ───────────────────────────────────────────────── -->
<input formControlName="title">
<textarea formControlName="body"></textarea>
<!-- ── FormArray section ─────────────────────────────────────────── -->
<div formArrayName="tags"> <!-- bind to the FormArray by name ──── -->
<h3>Tags</h3>
@for (ctrl of tagsArray.controls; track $index; let i = $index) {
<div class="tag-row">
<input [formControlName]="i" <!-- index as the control name ── -->
placeholder="Tag name"
[class.is-invalid]="ctrl.invalid && ctrl.touched">
@if (ctrl.hasError('maxlength')) {
<span class="error">Max 50 characters.</span>
}
<button type="button" (click)="removeTag(i)"
[disabled]="tagsArray.length === 1">×</button>
</div>
}
<button type="button" (click)="addTag()"
[disabled]="tagsArray.length >= 10">
+ Add Tag
</button>
</div>
<button type="submit" [disabled]="form.invalid">Save Post</button>
</form>
`,
})
export class PostFormComponent {
private fb = inject(FormBuilder);
form = this.fb.nonNullable.group({
title: ['', [Validators.required, Validators.minLength(5)]],
body: ['', [Validators.required, Validators.minLength(50)]],
// Typed FormArray of string controls
tags: this.fb.array([
this.fb.nonNullable.control('', Validators.maxLength(50)), // start with one
]),
});
// Typed accessor — cleaner than form.get('tags') as FormArray
get tagsArray(): FormArray<FormControl<string>> {
return this.form.controls.tags;
}
addTag(): void {
if (this.tagsArray.length < 10) {
this.tagsArray.push(this.fb.nonNullable.control('', Validators.maxLength(50)));
}
}
removeTag(index: number): void {
if (this.tagsArray.length > 1) {
this.tagsArray.removeAt(index);
}
}
onSubmit(): void {
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
const data = this.form.getRawValue();
// data.tags is string[] — non-empty tags only
const tags = data.tags.filter(t => t.trim().length > 0);
console.log('Submitting:', { ...data, tags });
}
}
formArrayName="tags" creates a binding to the FormArray. Inside it, [formControlName]="i" uses the array index as the control identifier. The variable i from the @for loop provides the index. This pattern — formArrayName + index-based formControlName — is the correct way to bind FormArray controls in templates. Do not use formControlName directly outside formArrayName context for array elements.get tagsArray()) to avoid casting form.get('tags') as FormArray repeatedly. The getter returns the strongly typed FormArray<FormControl<string>>, and TypeScript knows exactly what type tagsArray.controls[i].value is. This is much cleaner than the unsafe string-based access pattern. Similarly, for nested FormGroups, create getters: get addressGroup() { return this.form.controls.address; }.FormArray.controls is a reference — iterating it in a template with @for does not automatically update when controls are added or removed. Angular’s change detection handles this for OnPush components because pushing to a FormArray triggers change detection. However, if you are experiencing display issues after add/remove, verify that your FormArray operations go through Angular’s methods (push(), removeAt(), insert()) rather than directly modifying the underlying array.Common Mistakes
Mistake 1 — Accessing FormArray without a typed getter (runtime errors from as FormArray casts)
❌ Wrong — (form.get('tags') as FormArray).push(...) — unsafe cast, no type inference.
✅ Correct — typed getter: get tagsArray(): FormArray<FormControl<string>> { return this.form.controls.tags; }
Mistake 2 — Using formControlName without formArrayName context (template error)
❌ Wrong — [formControlName]="i" outside a formArrayName context; Angular cannot find the control.
✅ Correct — always wrap array control bindings inside a formArrayName parent.