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.

PointTemplate-driven formsReactive forms
Form defined inThe template, with ngModelThe class, with FormControl and FormGroup
ModuleFormsModuleReactiveFormsModule
Data modelUpdated by two-way bindingRead from the form: form.value
Adding fields at run timeDifficultEasy, with FormArray
Validation rulesAttributes in the templateFunctions in the class
Unit testingNeeds the templateTest the form object directly
Best forSmall, simple formsLarge, 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 standalone FormControl.
  • [formGroup] connects a <form> element to a FormGroup.
  • formControlName connects 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:

  1. Import ReactiveFormsModule into the component.
  2. Create a FormGroup with one FormControl per field in the class.
  3. Bind the <form> with [formGroup] and each field with formControlName.
  4. Handle (ngSubmit) and read form.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 FormBuilderWith 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' });
  }
}
PointsetValue()patchValue()
UpdatesEvery controlOnly the controls you pass
Missing keysThrows an errorAllowed
Typical useLoad a complete recordChange 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.
  • controls gives 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

MistakeWhat you seeFix
ReactiveFormsModule not importedNG0303: Can't bind to 'formGroup'Add ReactiveFormsModule to imports
formControlName outside a [formGroup]formControlName must be used with a parent formGroup directivePut the field inside the <form [formGroup]>
Misspelled control nameCannot find control with name: 'emial'Match the key in the FormGroup exactly
setValue() with a missing fieldMust supply a value for form control with name: 'role'Pass every field, or use patchValue()
Using [(ngModel)] and formControlName togetherA deprecation warning and confusing updatesUse one approach per form
Looping over skillsForm.get('skills') directlyA type error about AbstractControlAdd a getter that returns FormArray

Check Yourself

Exercises

What to Remember

  • Reactive forms define the form in the class with FormControl, FormGroup and FormArray, and need ReactiveFormsModule.
  • Bind with [formGroup], formControlName, formGroupName and formArrayName. FormBuilder creates the same objects with less code.
  • Use setValue() to replace every value and patchValue() to update some of them.

Next: Form Validation, how to check user input and show clear error messages.