Mastering Angular Forms: Building Dynamic and Validated User Input

Sagarnath S
8 min readJun 18, 2023
Building Dynamic and Validated User Input

Angular forms allow you to capture and validate user input in a structured and controlled manner. They provide a way to interact with users and gather data through various types of form controls such as input fields, checkboxes, radio buttons, and select dropdowns. Angular provides two approaches for building and handling forms.

Template-driven forms

A declarative approach to building forms where the form structure and validation rules are defined directly in the HTML template. This approach is intuitive and straightforward, making it suitable for simple forms with basic validation requirements.

To create a template-driven form in Angular, you use directives like ngModel and ngForm to bind form controls to properties in your component and handle form submission. Let's go through the process step by step with an example:

Import the necessary Angular modules: In your component file, import the FormsModule from @angular/forms to enable template-driven forms.

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

@NgModule({
imports: [
// Other module imports
FormsModule
],
// Component configuration
})
export class MyModule { }

Define the form structure in the HTML template: In the HTML template, you define the form structure using the <form> element and form controls such as <input>, <select>, and <textarea>. Bind these controls to properties in your component using the ngModel directive.

<form (ngSubmit)="onSubmit()">
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name" [(ngModel)]="user.name" required>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" [(ngModel)]="user.email" required email>
</div>
<button type="submit">Submit</button>
</form>

In this example, we have a simple form with two fields: name and email. The [(ngModel)] directive binds the input values to properties (user.name and user.email) in the component.

Handle form submission in the component: In the component file, define a method that will handle the form submission when the user clicks the submit button. In this method, you can access the form data from the component properties that were bound using ngModel.

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

@Component({
// Component configuration
})
export class MyComponent {
user = {
name: '',
email: ''
};

onSubmit() {
console.log('Form submitted:', this.user);
// Perform form submission logic
}
}

In this example, the onSubmit() method logs the form data to the console. You can perform your specific form submission logic inside this method.

Add validation to the form: Angular provides a set of built-in validators for common validation scenarios. You can apply these validators by adding attributes to the form controls in the template. In the example below, we apply the required validator to both name and email fields, and the email validator to the email field.

<form (ngSubmit)="onSubmit()">
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name" [(ngModel)]="user.name" required>
<div *ngIf="name.invalid && (name.dirty || name.touched)">
<div *ngIf="name.errors.required">Name is required.</div>
</div>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" [(ngModel)]="user.email" required email>
<div *ngIf="email.invalid && (email.dirty || email.touched)">
<div *ngIf="email.errors.required">Email is required.</div>
<div *ngIf="email.errors.email">Invalid email format.</div>
</div>
</div>
<button type="submit">Submit</button>
</form>

In this example, we use the *ngIf directive to conditionally display error messages based on the control's validity and its state (dirty or touched).

Template-driven forms in Angular provide a quick and easy way to build forms with basic validation. They are ideal for simple forms with straightforward requirements. However, for more complex forms with advanced validation needs and programmatic control, Reactive forms may be a better choice.

Reactive forms

Provide a programmatic approach to building forms by defining form controls and their validation rules in the component class. Reactive forms are more flexible and suitable for complex forms with advanced validation requirements.

To create a reactive form in Angular, you need to follow these steps. Let’s go through each step with an example:

Import the necessary Angular modules: In your component file, import the ReactiveFormsModule from @angular/forms to enable reactive forms.

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

@NgModule({
imports: [
// Other module imports
ReactiveFormsModule
],
// Component configuration
})
export class MyModule { }

Define the form controls and validation in the component: In the component class, define the form controls using the FormControl, FormGroup, and FormArray classes from @angular/forms. Set up the desired validators for each control using the Validators class.

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

@Component({
// Component configuration
})
export class MyComponent {
myForm: FormGroup;

constructor(private formBuilder: FormBuilder) {
this.myForm = this.formBuilder.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['']
});
}

onSubmit() {
if (this.myForm.valid) {
console.log('Form submitted:', this.myForm.value);
// Perform form submission logic
}
}
}

In this example, we create a reactive form with four controls: name, email, password, and confirmPassword. The Validators class is used to set up the required and validation rules for each control.

Bind the form controls in the template: In the HTML template, you can bind the form controls to the input elements using the formControlName directive.

<form [formGroup]="myForm" (ngSubmit)="onSubmit()">
<div>
<label for="name">Name:</label>
<input type="text" id="name" formControlName="name">
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" formControlName="email">
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" formControlName="password">
</div>
<div>
<label for="confirmPassword">Confirm Password:</label>
<input type="password" id="confirmPassword" formControlName="confirmPassword">
</div>
<button type="submit" [disabled]="myForm.invalid">Submit</button>
</form>

In this example, the formControlName directive is used to bind each form control to the respective input element.

Handle form submission in the component: The onSubmit() method is called when the form is submitted. Inside this method, you can access the form data using the myForm.value property.

onSubmit() {
if (this.myForm.valid) {
console.log('Form submitted:', this.myForm.value);
// Perform form submission logic
}
}

In this example, we log the form data to the console and perform the form submission logic only if the form is valid.

Reactive forms in Angular provide more control and flexibility when working with forms. You can define complex validation rules, dynamically add or remove form controls, and perform more programmatic operations on the form.

Form control grouping

It allows you to organize related form controls together. It provides a way to manage and validate a group of form controls as a single entity. There are two ways to group form controls in Angular: using FormGroup and FormArray. Let's go through each approach with examples:

FormGroup:

A FormGroup is used to group form controls together when they belong to the same logical form section. Here's an example:

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

@Component({
// Component configuration
})
export class MyComponent {
myForm: FormGroup;

constructor(private formBuilder: FormBuilder) {
this.myForm = this.formBuilder.group({
name: '',
address: this.formBuilder.group({
street: '',
city: '',
postalCode: ''
}),
contact: this.formBuilder.group({
email: '',
phone: ''
})
});
}
}

In this example, we create a FormGroup called myForm that contains three groups: address and contact. Each group contains its own set of form controls.

You can access the form controls and their values by using dot notation with the myForm object. For example, to access the street form control value, you would use this.myForm.get('address.street').value.

FormArray:

A FormArray is used to group an array of form controls together when you need to handle dynamic form controls, such as a list of items. Here's an example:

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

@Component({
// Component configuration
})
export class MyComponent {
myForm: FormGroup;

constructor(private formBuilder: FormBuilder) {
this.myForm = this.formBuilder.group({
name: '',
items: this.formBuilder.array([])
});
}

get items(): FormArray {
return this.myForm.get('items') as FormArray;
}

addItem(): void {
const newItem = this.formBuilder.group({
itemCode: '',
itemName: ''
});
this.items.push(newItem);
}
}

In this example, we create a FormArray called items within the myForm FormGroup. The items array will hold the dynamically added form controls.

The get items() method is used to retrieve the items FormArray from the myForm group.

The addItem() method is used to dynamically add a new item to the items array. It creates a new FormGroup for the item and pushes it to the items array.

You can access the form controls within the FormArray using the at() method. For example, to access the itemName form control of the first item, you would use this.items.at(0).get('itemName').value.

Form control grouping allows you to organize and manage related form controls together. It simplifies form control access, validation, and manipulation, especially for complex forms.

Cross-field validation

It refers to the validation of form controls that depend on the values of multiple fields. It allows you to enforce validation rules that involve interactions between different form controls. This can include scenarios such as password and confirm password matching, minimum and maximum value comparisons, or custom validation logic that relies on multiple fields.

Here’s an example of how to implement cross-field validation in Angular:

Create a custom validator function: In your component file, define a custom validator function that performs the cross-field validation logic. This function takes the form group as a parameter and returns a validation error object if the validation fails, or null if the validation passes.

import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';

export function passwordMatchValidator(): ValidatorFn {
return (formGroup: FormGroup): ValidationErrors | null => {
const passwordControl = formGroup.get('password');
const confirmPasswordControl = formGroup.get('confirmPassword');

if (passwordControl.value !== confirmPasswordControl.value) {
confirmPasswordControl.setErrors({ passwordMismatch: true });
} else {
confirmPasswordControl.setErrors(null);
}

return null;
};
}

In this example, the passwordMatchValidator function compares the values of the password and confirmPassword controls. If they don't match, it sets a custom error passwordMismatch on the confirmPassword control. If they match, it clears any errors on the confirmPassword control.

Apply the validator to the form group: In the component file, apply the custom validator to the form group that contains the form controls requiring cross-field validation.

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { passwordMatchValidator } from './password-match.validator';

@Component({
// Component configuration
})
export class MyComponent {
myForm: FormGroup;

constructor(private formBuilder: FormBuilder) {
this.myForm = this.formBuilder.group({
password: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['', Validators.required]
}, { validator: passwordMatchValidator() });
}
}

In this example, the password and confirmPassword controls are added to the form group. The passwordMatchValidator is applied to the form group using the validator property, which triggers the custom validation logic.

Display validation error messages: In the HTML template, you can display validation error messages based on the custom error set by the cross-field validation.

<form [formGroup]="myForm">
<div>
<label for="password">Password:</label>
<input type="password" id="password" formControlName="password">
</div>
<div>
<label for="confirmPassword">Confirm Password:</label>
<input type="password" id="confirmPassword" formControlName="confirmPassword">
<div *ngIf="myForm.hasError('passwordMismatch', 'confirmPassword')">
Password and Confirm Password do not match.
</div>
</div>
<button type="submit">Submit</button>
</form>

In this example, the error message is displayed when the confirmPassword control has the custom error passwordMismatch.

Cross-field validation allows you to define and enforce validation rules that depend on the values of multiple form controls. It gives you greater flexibility to implement complex validation scenarios and ensure data integrity in your forms.

Thank you for reading if you have any doubts. drop the message in comments.

Follow me on Medium or LinkedIn to read more about Angular and TS!

--

--

Sagarnath S

Software Development Engineer - I @CareStack || Web Developer || Angular || ASP.Net Core || C# || TypeScript || HTML || CSS