Reactive Forms
By Syncfusion®, Inc. · Last updated
Defining forms in the component class with FormControl, FormGroup, FormBuilder and FormArray, setting values with setValue and patchValue, and nesting groups.
What Are Reactive Forms?
A reactive form is a form whose structure, values and rules are defined in the component class, as objects. The template only connects its fields to those objects.
Because the form is a plain object in the class, your code can read it, change it, test it and add fields to it at any time, without touching the template.
Real-World Example
A job application form asks for personal details, a postal address, and a list of previous employers. The user can click Add employer as many times as needed.
The class defines the address as a group of fields and the employers as a list that grows at run time. When the user submits, the class sends one nested object to the API.
FormGroupapplicationFormnameFormControladdressFormGroupemployersFormArraycitypin[0][1]+ add
Controls hold values, groups combine them, and arrays hold lists that grow at run time.
Template-Driven Forms vs Reactive Forms
Both approaches use the same browser fields and the same states, such as dirty and valid. They differ in where the form is defined.
| Point | Template-driven forms | Reactive forms |
|---|---|---|
| Form defined in | The template, with ngModel | The class, with FormControl and FormGroup |
| Module | FormsModule | ReactiveFormsModule |
| Data model | Updated by two-way binding | Read from the form: form.value |
| Adding fields at run time | Difficult | Easy, with FormArray |
| Validation rules | Attributes in the template | Functions in the class |
| Unit testing | Needs the template | Test the form object directly |
| Best for | Small, simple forms | Large, dynamic or complex forms |
Choose one approach per form. Do not mix ngModel and formControlName in the same form, because both would try to control the same field.
ReactiveFormsModule
ReactiveFormsModule provides the directives that connect template fields to form objects in the class:
[formControl]connects one field to a standaloneFormControl.[formGroup]connects a<form>element to aFormGroup.formControlNameconnects a field inside that form to one control of the group.
Import it from @angular/forms and add it to the component's imports list.
import { ReactiveFormsModule } from '@angular/forms';
@Component({
imports: [ReactiveFormsModule],
// ...
})
FormControl
A FormControl is an object that holds the value and state of one field. You create it in the class with an initial value, and connect it in the template with [formControl].
Read the current value with control.value. The valueChanges property emits every new value, so code can react to typing.
A FormControl holds one field's value
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [ReactiveFormsModule],
template: `
<label>Delivery city: <input [formControl]="city" /></label>
<p>Value: {{ city.value }}</p>
<p>Changed by the user: {{ city.dirty }}</p>
<button (click)="city.setValue('Chennai')">Set to Chennai</button>
`,
})
export class App {
city = new FormControl('Pune');
}
FormGroup
A FormGroup combines several controls into one object. Its value is an object with one key for each control, and it is valid only when every control is valid.
In the template, bind the group to the <form> with [formGroup], and each field to a control with formControlName.
A FormGroup combines several controls
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-root',
imports: [ReactiveFormsModule, JsonPipe],
template: `
<form [formGroup]="loginForm">
<input formControlName="username" placeholder="Username" />
<input formControlName="password" type="password" placeholder="Password" />
</form>
<pre>{{ loginForm.value | json }}</pre>
`,
})
export class App {
loginForm = new FormGroup({
username: new FormControl('asha.k'),
password: new FormControl(''),
});
}
Creating Reactive Forms
Creating a reactive form takes four steps:
- Import
ReactiveFormsModuleinto the component. - Create a
FormGroupwith oneFormControlper field in the class. - Bind the
<form>with[formGroup]and each field withformControlName. - Handle
(ngSubmit)and readform.value.
A complete reactive form, from definition to submit
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="ticketForm" (ngSubmit)="submit()">
<p><input formControlName="title" placeholder="Issue title" /></p>
<p>
<select formControlName="priority">
<option value="low">Low</option>
<option value="high">High</option>
</select>
</p>
<button type="submit">Create ticket</button>
</form>
<p>{{ result }}</p>
`,
})
export class App {
ticketForm = new FormGroup({
title: new FormControl('Payment page timeout'),
priority: new FormControl('high'),
});
result = '';
submit() {
const ticket = this.ticketForm.value;
this.result = 'Created "' + ticket.title + '" with ' + ticket.priority + ' priority';
}
}
FormBuilder
FormBuilder is a service that creates controls, groups and arrays with less code. Inject it with inject(FormBuilder).
fb.group({ name: [''] }) creates the same FormGroup as new FormGroup({ name: new FormControl('') }). Each control is written as an array: the first item is the initial value, and the second item holds validators, covered in the next lesson.
FormBuilder creates the same form with shorter code
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-root',
imports: [ReactiveFormsModule, JsonPipe],
template: `
<form [formGroup]="profileForm">
<input formControlName="firstName" />
<input formControlName="lastName" />
<input formControlName="phone" />
</form>
<pre>{{ profileForm.value | json }}</pre>
`,
})
export class App {
private fb = inject(FormBuilder);
profileForm = this.fb.group({
firstName: ['Ravi'],
lastName: ['Shankar'],
phone: ['98450 12345'],
});
}
Without FormBuilder | With FormBuilder |
|---|---|
new FormGroup({ ... }) | fb.group({ ... }) |
new FormControl('Ravi') | ['Ravi'] |
new FormArray([ ... ]) | fb.array([ ... ]) |
Setting and Updating Form Values
You often need to fill a form from code, for example after loading a saved record from an API. Reactive forms provide two methods.
setValue()
setValue() replaces the value of the whole group. The object must contain every control of the group. If a key is missing or extra, Angular throws an error.
patchValue()
patchValue() updates only the controls you include. Other controls keep their current values. Use it for partial updates.
setValue needs every field, patchValue accepts some
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-root',
imports: [ReactiveFormsModule, JsonPipe],
template: `
<form [formGroup]="userForm">
<input formControlName="name" />
<input formControlName="email" />
<input formControlName="role" />
</form>
<button (click)="loadUser()">Load saved user (setValue)</button>
<button (click)="changeRole()">Promote (patchValue)</button>
<pre>{{ userForm.value | json }}</pre>
`,
})
export class App {
userForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
role: new FormControl(''),
});
loadUser() {
this.userForm.setValue({ name: 'Meena', email: 'meena@example.com', role: 'editor' });
}
changeRole() {
this.userForm.patchValue({ role: 'admin' });
}
}
| Point | setValue() | patchValue() |
|---|---|---|
| Updates | Every control | Only the controls you pass |
| Missing keys | Throws an error | Allowed |
| Typical use | Load a complete record | Change one or two fields |
Nested Form Groups
A FormGroup can contain another FormGroup. Nesting keeps related fields together, such as the parts of an address, and produces a nested object in form.value.
In the template, wrap the nested fields in an element with formGroupName.
A nested group produces a nested value
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-root',
imports: [ReactiveFormsModule, JsonPipe],
template: `
<form [formGroup]="customerForm">
<p><input formControlName="name" /></p>
<fieldset formGroupName="address">
<legend>Address</legend>
<input formControlName="city" />
<input formControlName="pin" />
</fieldset>
</form>
<pre>{{ customerForm.value | json }}</pre>
`,
})
export class App {
private fb = inject(FormBuilder);
customerForm = this.fb.group({
name: ['Asha Kumar'],
address: this.fb.group({
city: ['Mumbai'],
pin: ['400001'],
}),
});
}
FormArray
A FormArray holds a list of controls or groups whose length can change at run time. Use it for repeating fields, such as phone numbers, skills or order items.
push(control)adds an item.removeAt(index)removes an item.controlsgives the list to loop over in the template.
In the template, wrap the list in an element with formArrayName, and bind each item by its index with [formControlName]="i".
FormArray adds and removes fields at run time
import { Component, inject } from '@angular/core';
import { FormArray, FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="skillsForm">
<div formArrayName="skills">
@for (skill of skills.controls; track skill; let i = $index) {
<p><input [formControlName]="i" /> <button type="button" (click)="skills.removeAt(i)">Remove</button></p>
}
</div>
</form>
<button (click)="addSkill()">Add skill</button>
<p>Skills: {{ skills.value.join(', ') }}</p>
`,
})
export class App {
private fb = inject(FormBuilder);
skillsForm = this.fb.group({
skills: this.fb.array([['Angular'], ['TypeScript']]),
});
get skills() {
return this.skillsForm.get('skills') as FormArray;
}
addSkill() {
this.skills.push(new FormControl('New skill'));
}
}
The skills getter returns the array with the right type, so the template and methods can call push, removeAt and controls.
Use a get accessor for every FormArray and nested group you use often. It keeps the template short and avoids repeating form.get('...').
Reactive form values are typed. new FormControl('Pune') has the type FormControl<string | null>, so TypeScript reports a mistake such as city.value.toFixed(). The value can be null because reset() sets it to null. Use new FormControl('Pune', { nonNullable: true }) to keep it a string.
Common Mistakes
| Mistake | What you see | Fix |
|---|---|---|
ReactiveFormsModule not imported | NG0303: Can't bind to 'formGroup' | Add ReactiveFormsModule to imports |
formControlName outside a [formGroup] | formControlName must be used with a parent formGroup directive | Put the field inside the <form [formGroup]> |
| Misspelled control name | Cannot find control with name: 'emial' | Match the key in the FormGroup exactly |
setValue() with a missing field | Must supply a value for form control with name: 'role' | Pass every field, or use patchValue() |
Using [(ngModel)] and formControlName together | A deprecation warning and confusing updates | Use one approach per form |
Looping over skillsForm.get('skills') directly | A type error about AbstractControl | Add a getter that returns FormArray |
Check Yourself
Exercises
What to Remember
- Reactive forms define the form in the class with
FormControl,FormGroupandFormArray, and needReactiveFormsModule. - Bind with
[formGroup],formControlName,formGroupNameandformArrayName.FormBuildercreates the same objects with less code. - Use
setValue()to replace every value andpatchValue()to update some of them.
Next: Form Validation, how to check user input and show clear error messages.