DEV Community

Cover image for Angular Reactive Forms Basics
Mirza Leka
Mirza Leka

Posted on

Angular Reactive Forms Basics

Angular Reactive Forms is a feature provided by the Angular framework that allows you to create and manage complex forms reactively. Reactive forms provide a model-driven approach to handling form inputs whose values change over time.

Why use Reactive forms?

Model-Driven approach

With Reactive forms, you create a form model, controls, and validations in the TypeScript component and bind it to the template.

Declarative dynamic behavior

Once the form model is created and is wired with the template, any changes made to the form controls are automatically reflected in the form's state, without the need for explicit event handling.

Observables

Reactive Forms in Angular heavily utilize observables. Each form control is represented as an observable stream of values that you can operate on (using Rx.js operators).

Form Controls

Each form field is a form control that is part of the form group. You declare the controls within the component and set a default value and validation rules for each. Controls and validations can be dynamically added or removed as well.

Creating your first form

Step 1: Importing ReactiveFormsModule in your App Module:

@NgModule({
  imports: [
    // other imports...
    ReactiveFormsModule
  ],
  // other declarations and providers...
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

If you're using Standalone components then you import the module into the imports array:

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

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent
Enter fullscreen mode Exit fullscreen mode

Step 2: Setup a form using FormBuilder

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent implements OnInit {
  myForm!: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.fb.group({ // Form Group
      username: [''], // Form Control
      email: [''], // Form Control
      password: [''], // Form Control
    });
  }

  onSubmit() {
    console.log('Form values: ', this.myForm.value);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3 - Binding Form to the template

As we can see the form group contains a list of form controls. We're going to use the form group and the controls to bind each field in the HTML template.

<form [formGroup]="myForm" (ngSubmit)="onSubmit()">
  <div>
    <label>
      Username:
      <input formControlName="username" placeholder="YOUR PLACEHOLDER">
    </label>
  </div>

  <div>
    <label>
      Email:
      <input formControlName="email" placeholder="">
    </label>
  </div>

  <div>
    <label>
      Password:
      <input formControlName="password" placeholder="*******">
    </label>
  </div>

  <button type="submit">Create</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Using (ngSubmit)="onSubmit()" we'll call the onSubmit() function in the TypeScript every time we click the submit button.

form-submit-preview

Step 4 - Adding Validations

When creating new form controls you can set the starter (default) value to the control:

    this.myForm = this.fb.group({
      username: ['E.g. SuperUser'], // Form Control
      ...
    });
Enter fullscreen mode Exit fullscreen mode

Following the starter value, you can add a set of synchronous validations:

    this.myForm = this.fb.group({
      username: ['E.g. SuperUser', [Validators.required]],
      email: ['', [Validators.required, Validators.email]],
      password: [
        '',
        [
          Validators.required,
          Validators.minLength(3),
          Validators.pattern('<REGEX>'),
        ],
      ],
    });
Enter fullscreen mode Exit fullscreen mode

There is a set of built-in validations:

  • required
  • valid email
  • min/max length
  • follow a specific pattern, etc.

But you can also create your custom validators
Additionally, you can add Async validators that validate data against APIs.

    this.myForm = this.fb.group({
      username: [
        '',
        [
          /* Sync validators */
        ],
        [
          /* Async validators */
        ],
      ],
     ....
Enter fullscreen mode Exit fullscreen mode

The typical use case of an async validator would be the search functionality. As you type your input into a search field, Angular will send data to the backend, check if there is a match, and return the response in real-time, e.g., check if a username exists while typing on the sign-up form.

Step 5 - Applying form validations in the HTML template

Now we tell Angular to display an error for each control when it is invalid.

   <div>
      <label>
        Email:
        <input formControlName="email" placeholder="">
        <div 
          style="color: red;" 
          *ngIf="myForm.get('email')?.invalid">
            Please provide a valid email address.
          </div>
      </label>
    </div>
Enter fullscreen mode Exit fullscreen mode

email-error

To keep errors from popping up as soon as the page loads, you can apply validations only when certain conditions are met:

  • form control is invalid
  • form control is clicked on (touched)
  • form control has been typed on (dirty)
    <div>
      <label>
        Email:
        <input formControlName="email" placeholder="">
        <div
            style="color: red;"
            *ngIf="myForm.get('email')?.invalid && (myForm.get('email')?.dirty || myForm.get('email')?.touched)">
            Please provide a valid email address.
          </div>
      </label>
    </div>
Enter fullscreen mode Exit fullscreen mode

before-after

What I like to do is create getter functions for each control and wrap the validation logic:

  get isInvalidEmail(): boolean {
    return !!(
      this.myForm.get('email')?.invalid &&
      (this.myForm.get('email')?.dirty || this.myForm.get('email')?.touched)
    );
  }
Enter fullscreen mode Exit fullscreen mode

And apply it in the template:

    <div>
      <label>
        Email:
        <input formControlName="email" placeholder="">
        <div
            style="color: red;"
            *ngIf="isInvalidEmail">
            Please provide a valid email address.
          </div>
      </label>
    </div>
Enter fullscreen mode Exit fullscreen mode

More Reactive form goodies

Inspect form

Read form value

this.myForm.value
Enter fullscreen mode Exit fullscreen mode

Check form validity

this.myForm.valid // true / false
Enter fullscreen mode Exit fullscreen mode

Inspect form interactions

this.myForm.status // INVALID / VALID
this.myForm.dirty // true / false
this.myForm.touched // true / false
this.myForm.pristine // true / false
this.myForm.untouched// true / false
Enter fullscreen mode Exit fullscreen mode

Look up form controls

this.myForm.get('username') // Abstract Control
this.myForm.get('email') // Abstract Control
this.myForm.get('password') // Abstract Control
Enter fullscreen mode Exit fullscreen mode

Alter Form Behavior

Clear form values

this.myForm.reset();
Enter fullscreen mode Exit fullscreen mode

Clear errors

this.myForm.setErrors(null);
Enter fullscreen mode Exit fullscreen mode

Mark as dirty

this.myForm.markAsDirty();
Enter fullscreen mode Exit fullscreen mode

Mark as touched

this.myForm.markAsTouched();
Enter fullscreen mode Exit fullscreen mode

Update validity

this.myForm.updateValueAndValidity();
Enter fullscreen mode Exit fullscreen mode

As well as add/remove controls and validators.

Using the same methods for FormGroup, you can inspect or alter individual controls:

this.myForm.get('username')?.value;
this.myForm.get('username')?.valid;
this.myForm.get('username')?.touched;
this.myForm.get('username')?.dirty;
this.myForm.get('username')?.markAsDirty();
this.myForm.get('username')?.patchValue('New value');
this.myForm.get('username')?.setErrors(null);
this.myForm.get('username')?.hasError('required') // true / false
// and so on...
Enter fullscreen mode Exit fullscreen mode

Use of Rx.js

Form Groups and Abstract Controls have a valueChanges property that converts form value into an Observable stream.

    this.myForm.get('password')?.valueChanges
      .pipe(
        /* Rx.js operators */
      )
      .subscribe((data: string) => {
        console.log('data :>> ', data);
      })
Enter fullscreen mode Exit fullscreen mode

An example scenario would be to replace each character with an asterisk:

import { debounceTime, distinctUntilChanged, map } from 'rxjs';

    const passwordControl = this.myForm.get('password');

    passwordControl?.valueChanges
      .pipe(
        // replace all characters with asterisk
        map(currentValue => currentValue.replace(/./g, '*'))
      )
      .subscribe((newValue: string) => {
        // Observables are immutable, which is why we need to update form control with new value
        // We're using { emitEvent: false } to prevent triggering valueChanges after patchValue
        passwordControl.patchValue(newValue, { emitEvent: false })
      })

// P.S. <input type="password" does this automatically
Enter fullscreen mode Exit fullscreen mode

Or emit values to an Observable only after user has stopped typing:

    this.myForm.get('searchText')?.valueChanges
      .pipe(
        // emit 500ms after user stopped typing
        debounceTime(500),
        // emit only when value changes
        distinctUntilChanged()
      )
      .subscribe((data: string) => {
        console.log('data :>> ', data);
      })
Enter fullscreen mode Exit fullscreen mode

If you prefer not to use Observables, you can always use getRawValue() method.

    const val = this.myForm.get('password')?.getRawValue();
    console.log('val :>> ', val);
Enter fullscreen mode Exit fullscreen mode

Next Chapter ➡️

That's all for today!

For everything else awesome, follow me on Dev.to and on Twitter to stay up to date with my content updates.

Top comments (0)