Angular Reactive Forms

Angular Forms

Angular framework provides two ways to work with forms – template-driven forms and reactive forms,

which are also known as model-driven forms.

Template-driven forms are the default forms where template directives are used to build an internal representation of the form.

Template-driven forms let you directly modify data in your template, but are less explicit than reactive forms because they rely on directives embedded in the template, along with mutable data to track changes asynchronously.

With reactive forms, you build your own representation of a form in the component class.

Reactive forms use an explicit and immutable approach for managing the state of a form at a given point in time. It means that each change to the form state returns a new state. Reactive forms are built around observable streams, where form inputs and values are provided as streams of input values, which can be accessed synchronously.

Reactive forms also provide a straightforward path to testing because you are assured that your data is consistent and predictable when requested. Any consumers of the streams have access to manipulate that data safely.

Here are some of the advantages of using reactive forms in Angular:

  • You can use custom validators
  • You can change validation dynamically
  • You can add form fields dynamically

Prerequisites

Angular 20, Npm 10.9.2, Node 22.17.0

Project setup

Create a new project by executing the following command:

ng new angular-reactive-forms --no-standalone

The --no-standalone flag is used with the Angular CLI’s ng new command to create a new Angular application that uses NgModules instead of standalone components.

Now you can choose whether you want to create the application without zone or not. I have chosen to use zone, so I put N.

I have chosen CSS for style sheet. I have chosen y for Server Side Rendering and static page generation.

Adding Form Control

To work with reactive form import ReactiveFormsModule instead of FormsModule from the @angular/forms package and add it to your app-module.ts file:

import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core';
import { BrowserModule, provideClientHydration, withEventReplay } from '@angular/platform-browser';

import { ReactiveFormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing-module';
import { App } from './app';

@NgModule({
  declarations: [
    App
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
	ReactiveFormsModule
  ],
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideClientHydration(withEventReplay())
  ],
  bootstrap: [App]
})
export class AppModule { }

Adding a form to the template

The logic is declared entirely in the component class while working with the reactive form.

Open template file app.html in your code editor and add the following lines of code:

<form [formGroup]="registrationForm" (ngSubmit)="onSubmit(registrationForm)">
  <div>
    <label>
      Full Name:
      <input formControlName="name" placeholder="Full name">
    </label>
  </div>
  <div>
    <label>
      Email:
      <input formControlName="email" placeholder="Your email">
    </label>
  </div>
  <div>
    <label>
      Address:
      <input formControlName="address" placeholder="Your address">
    </label>
  </div>
  <div>
    <label>
      Country:
      <input formControlName="country" placeholder="Your country">
    </label>
  </div>
  <div>
    <label>
      Postal Code:
      <input formControlName="pin" placeholder="Your pin code">
    </label>
  </div>
  <button type="submit">Register</button>
</form>

This code will create a form with five fields: name, email, address, country and pin code. There is also a “submit” button with the label “Register”. When submitting the form, the method onSubmit(registrationForm) will be called.

formGroup: The formGroup directive allows to give a name to the form group and the form is treated as a FormGroup in the component class.

ngSubmit: This is an event that will be triggered upon submission of the form.

formControlName: Each form field should have a formControlName directive with a value, which is actually the name used in the component class.

Building the component class

Next, in the component class app.ts file, you need to define the FormGroup and individual FormControls within the FormGroup.

If a value is provided when a new instance of FormControl is created, it will be used as the initial value for the field.

Notice the FormGroup and FormControl names are same that you used in the template file. Also notice how you initialize the FormGroup in the ngOnInit lifecycle hook.

import { Component, signal, OnInit } from '@angular/core';

import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  standalone: false,
  styleUrl: './app.css'
})
export class App implements OnInit {
  protected readonly title = signal('Angular Reactive Forms');
  
  registrationForm!: FormGroup;

  ngOnInit() {
    this.registrationForm = new FormGroup({
      name: new FormControl('Roytuts'),
      email: new FormControl('[email protected]'),
      address: new FormControl(''),
	  country: new FormControl(''),
	  pin: new FormControl('')
    });
  }

  onSubmit(form: FormGroup) {
    console.log('Valid: ', form.valid);
    console.log('Name: ', form.value.name);
    console.log('Email: ', form.value.email);
    console.log('Address: ', form.value.address);
	console.log('Country: ', form.value.country);
    console.log('Postal Code: ', form.value.pin);
  }
  
}

For this example of tutorial, the onSubmit method does not actually communicate the submitted form values to any external service or server. It serves to show you how you can access the form’s validity and FormControl values.

At this point, you can compile your application and open it in a web browser. After entering values for name, email, address, country and pin code and pressing Submit, the console log will display the values.

You will see the following error thrown in the console of the server:

Property ‘registrationForm’ has no initializer and is not definitely assigned in the constructor.

The solution is to use the ! (Definite Assignment Assertion) Operator: If you are certain that the FormGroup will be initialized later (e.g., in ngOnInit), you can use the ! operator to tell TypeScript to relax the definite assignment check. However, this should be used with caution, as it bypasses TypeScript’s type safety. That’s why I have used !registrationForm!: FormGroup;

Updating the component class to use FormBuilder

The ngOnInit form construction can be rewritten with the help of FormBuilder. This allows you to forgo of all the new keywords of form group and form controls.

Now open the app.ts in your code editor and remove the FormControl and replace the FormGroup with the FormBuilder:

import { Component, signal, OnInit } from '@angular/core';





import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  standalone: false,
  styleUrl: './app.css'
})
export class App implements OnInit {
  protected readonly title = signal('Angular Reactive Forms');
  
  registrationForm!: FormGroup;
  
  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.registrationForm = this.fb.group({
      name: 'Roytuts',
      email: '[email protected]',
      address: '',
	  country: '',
	  pin: ''
    });
  }

  onSubmit(form: FormGroup) {
    console.log('Valid: ', form.valid);
    console.log('Name: ', form.value.name);
    console.log('Email: ', form.value.email);
    console.log('Address: ', form.value.address);
	console.log('Country: ', form.value.country);
    console.log('Postal Code: ', form.value.pin);
  } 
  
}

Or even you can write the form builder code inside constructor instead of ngOnInit lifecycle hook:

import { Component, signal, OnInit } from '@angular/core';

import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  standalone: false,
  styleUrl: './app.css'
})
export class App implements OnInit {
  protected readonly title = signal('Angular Reactive Forms');
  
  registrationForm: FormGroup;
  
  constructor(private fb: FormBuilder) {
	this.registrationForm = this.fb.group({
      name: 'Roytuts',
      email: '[email protected]',
      address: '',
	  country: '',
	  pin: ''
    });
  }

  ngOnInit(): void {
  }

  onSubmit(form: FormGroup) {
    console.log('Valid: ', form.valid);
    console.log('Name: ', form.value.name);
    console.log('Email: ', form.value.email);
    console.log('Address: ', form.value.address);
	console.log('Country: ', form.value.country);
    console.log('Postal Code: ', form.value.pin);
  } 
  
}

Note you don’t need to use the ! (Definite Assignment Assertion) Operator when you initialize the form builder inside the constructor instead of ngOnInit lifecycle hook.

Use validators in component class

Add the Validators class to your imports and declare your form controls with arrays instead of simple string values.

The first value in the array is the initial form value and the second value is for the validator(s) to use. Notice how multiple validators can be used on the same form control by wrapping them into an array.

import { Component, signal, OnInit } from '@angular/core';

import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  standalone: false,
  styleUrl: './app.css'
})
export class App implements OnInit {
  protected readonly title = signal('Angular Reactive Forms');
  
  registrationForm!: FormGroup;
  
  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.registrationForm = this.fb.group({
      name: ['Roytuts', Validators.required],
      email: ['[email protected]', [Validators.required, Validators.email]],
      address: ['', [Validators.required, Validators.minLength(30)]],
	  country: ['', [Validators.required, Validators.minLength(2)]],
	  pin: ['', [Validators.required, Validators.minLength(5)]]
    });
  }

  onSubmit(form: FormGroup) {
    console.log('Valid: ', form.valid);
    console.log('Name: ', form.value.name);
    console.log('Email: ', form.value.email);
    console.log('Address: ', form.value.address);
	console.log('Country: ', form.value.country);
    console.log('Postal Code: ', form.value.pin);
  } 
  
}

This code adds required to the name, email, address, country and postal code fields. It also ensures the email value uses the format of a valid email address. It also ensures that address, country and pin value should have their respective minimum length of characters.

If any of these form requirements are not passing, the valid value will be false. If all of these form requirements are passing, the valid value will be true.

Acessing form value and validating form in template

You have added the validation in the component class but if your validation fails you are not able to see the validation errors in the web page, so you need to add the validation in the template file.

Use *ngIf in app.html to display feedback messages to the user if the form values are not valid.

<form [formGroup]="registrationForm" (ngSubmit)="onSubmit(registrationForm)">
  <div>
    <label>
      Full Name:
      <input id="name" formControlName="name" placeholder="Full name">
    </label>
	<div *ngIf="registrationForm.get('name')?.invalid && (registrationForm.get('name')?.dirty || registrationForm.get('name')?.touched)">
      Please provide a full name.
    </div>
  </div>
  <div>
    <label>
      Email:
      <input id="email" formControlName="email" placeholder="Your email">
    </label>
	<div *ngIf="registrationForm.get('email')?.invalid && (registrationForm.get('email')?.dirty || registrationForm.get('email')?.touched)">
      Please provide a valid email address.
    </div>
  </div>
  <div>
    <label>
      Address:
      <textarea id="address" formControlName="address" placeholder="Your address"></textarea>
    </label>
	<div *ngIf="registrationForm.get('address')?.invalid && (registrationForm.get('address')?.dirty || registrationForm.get('address')?.touched)">
      Address must be at least 30 characters long.
    </div>
  </div>
  <div>
    <label>
      Country:
      <input id="country" formControlName="country" placeholder="Your country">
    </label>
	<div *ngIf="registrationForm.get('country')!.invalid && (registrationForm.get('country')!.dirty || registrationForm.get('country')!.touched)">
      Please provide country name with at least two characters.
    </div>
  </div>
  <div>
    <label>
      Postal Code:
      <input id="pin" formControlName="pin" placeholder="Your pin code">
    </label>
	<div *ngIf="registrationForm.get('pin')?.invalid && (registrationForm.get('pin')?.dirty || registrationForm.get('pin')?.touched)">
      Please provide postal code with at least 5 digits.
    </div>
  </div>
  <button type="submit" [disabled]="registrationForm.invalid">Register</button>
</form>

<router-outlet />

In the above template file notice I have put ? or ! in the *ngIf condition otherwise you will see the following error:

Object is possibly ‘null’

When you use formGroup.get('controlName'), the return type is AbstractControl | null. This means that if controlName does not exist in the formGroup, get() will return null. Directly accessing properties like touched or dirty on a potentially null object will lead to a runtime error if the object is indeed null.

This code checks to see if the user has interacted with the field (dirty or touched). Then, if the value is not passed to the input fields or criteria is not matched with the validation requirements, it will display the error messages. The Register button will remain disabled until you fullfil the input criteria.

angular reactive form

Source Code

Download

Share

Related posts

No comments

Leave a comment