Skip to main content

Angular Forms Introduction

Forms are an essential part of web applications, enabling users to input data and interact with your application. Angular provides robust tools for creating and managing forms that go well beyond traditional HTML forms. In this guide, we'll introduce you to Angular's form capabilities and show you how to implement them in your projects.

Why Forms in Angular?

HTML forms work well for simple applications, but as applications grow in complexity, you need more sophisticated ways to:

  • Track form and input states (pristine, dirty, valid, invalid)
  • Validate user input and display validation messages
  • Manage the form's data model
  • Test form interactions

Angular provides these capabilities through two distinct approaches to forms.

The Two Types of Angular Forms

Angular offers two different approaches to handle forms:

  1. Template-driven forms: Simpler, similar to Angular.js, but less flexible
  2. Reactive forms: More robust, explicit, and flexible

Let's look at the key differences:

FeatureTemplate-driven FormsReactive Forms
Form model setupCreated automaticallyExplicitly defined in code
Data flowTwo-way bindingReactive patterns
Form validationDirectives in templateFunctions in component
ScalabilityGood for simple formsBetter for complex forms
TestingMore challengingEasier (more explicit code)
SyntaxMore HTML, less TypeScriptMore TypeScript, less HTML

Setting Up Forms in Your Angular Project

Before using either form approach, you need to import the appropriate Angular module:

typescript
// For template-driven forms (in app.module.ts)
import { FormsModule } from '@angular/forms';

@NgModule({
imports: [
FormsModule
],
// other module properties
})
export class AppModule { }
typescript
// For reactive forms (in app.module.ts)
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
imports: [
ReactiveFormsModule
],
// other module properties
})
export class AppModule { }

Template-driven Forms: A Simple Example

Template-driven forms are great for simple scenarios. Here's a basic login form:

html
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm.value)">
<div>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
ngModel
required
email>
</div>

<div>
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
ngModel
required>
</div>

<button type="submit" [disabled]="!loginForm.valid">Log In</button>
</form>
typescript
import { Component } from '@angular/core';

@Component({
selector: 'app-login',
templateUrl: './login.component.html'
})
export class LoginComponent {
onSubmit(formData: any) {
console.log('Form submitted with:', formData);
// { email: '[email protected]', password: 'secretpassword' }
}
}

In this example, Angular:

  • Creates form controls implicitly with ngModel
  • Tracks validity with built-in validators (required, email)
  • Disables the submit button when the form is invalid

Reactive Forms: A Simple Example

Let's implement the same login form using reactive forms:

html
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div>
<label for="email">Email</label>
<input type="email" id="email" formControlName="email">
<div *ngIf="email.invalid && (email.dirty || email.touched)">
<div *ngIf="email.errors?.['required']">Email is required.</div>
<div *ngIf="email.errors?.['email']">Please enter a valid email.</div>
</div>
</div>

<div>
<label for="password">Password</label>
<input type="password" id="password" formControlName="password">
<div *ngIf="password.invalid && (password.dirty || password.touched)">
<div *ngIf="password.errors?.['required']">Password is required.</div>
</div>
</div>

<button type="submit" [disabled]="loginForm.invalid">Log In</button>
</form>
typescript
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
selector: 'app-login',
templateUrl: './login.component.html'
})
export class LoginComponent implements OnInit {
loginForm: FormGroup;

constructor(private fb: FormBuilder) { }

ngOnInit() {
this.loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required]
});
}

get email() { return this.loginForm.get('email'); }
get password() { return this.loginForm.get('password'); }

onSubmit() {
console.log('Form submitted with:', this.loginForm.value);
// { email: '[email protected]', password: 'secretpassword' }
}
}

In this reactive approach:

  • Form controls are explicitly defined in the component
  • Validation rules are defined in the component
  • Detailed validation errors can be displayed conditionally

Form Control States

Both types of Angular forms track several states for each form control:

StateDescriptionCSS Classes
Pristine/DirtyWhether the value has changedng-pristine or ng-dirty
Touched/UntouchedWhether the control has been focusedng-touched or ng-untouched
Valid/InvalidWhether the value passes validation rulesng-valid or ng-invalid

You can use these states to provide visual feedback to users:

css
input.ng-invalid.ng-touched {
border: 1px solid red;
}

input.ng-valid.ng-dirty {
border: 1px solid green;
}

Real-world Example: User Registration Form

Let's create a more comprehensive registration form using reactive forms:

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

@Component({
selector: 'app-registration',
templateUrl: './registration.component.html'
})
export class RegistrationComponent implements OnInit {
registrationForm: FormGroup;
submitted = false;

constructor(private fb: FormBuilder) { }

ngOnInit() {
this.registrationForm = this.fb.group({
personalDetails: this.fb.group({
firstName: ['', [Validators.required, Validators.minLength(2)]],
lastName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
}),
accountDetails: this.fb.group({
username: ['', [Validators.required, Validators.minLength(4)]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}),
acceptTerms: [false, Validators.requiredTrue]
}, {
validator: this.passwordMatchValidator
});
}

// Custom validator to check if passwords match
passwordMatchValidator(fg: FormGroup) {
const password = fg.get('accountDetails.password')?.value;
const confirmPassword = fg.get('accountDetails.confirmPassword')?.value;

if (password !== confirmPassword) {
fg.get('accountDetails.confirmPassword')?.setErrors({ passwordMismatch: true });
return { passwordMismatch: true };
}
return null;
}

onSubmit() {
this.submitted = true;

if (this.registrationForm.invalid) {
return;
}

console.log('Registration successful!', this.registrationForm.value);
// Submit to API, etc.
}

// Getters for easy access in the template
get f() { return this.registrationForm.controls; }
get personalDetails() { return this.registrationForm.get('personalDetails') as FormGroup; }
get accountDetails() { return this.registrationForm.get('accountDetails') as FormGroup; }
}
html
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
<h3>Personal Details</h3>
<div formGroupName="personalDetails">
<div>
<label for="firstName">First Name</label>
<input type="text" id="firstName" formControlName="firstName">
<div *ngIf="personalDetails.get('firstName').invalid &&
(personalDetails.get('firstName').dirty || personalDetails.get('firstName').touched || submitted)">
<div *ngIf="personalDetails.get('firstName').errors?.['required']">First name is required</div>
<div *ngIf="personalDetails.get('firstName').errors?.['minlength']">
First name must be at least 2 characters
</div>
</div>
</div>

<div>
<label for="lastName">Last Name</label>
<input type="text" id="lastName" formControlName="lastName">
<div *ngIf="personalDetails.get('lastName').invalid &&
(personalDetails.get('lastName').dirty || personalDetails.get('lastName').touched || submitted)">
<div *ngIf="personalDetails.get('lastName').errors?.['required']">Last name is required</div>
</div>
</div>

<div>
<label for="email">Email</label>
<input type="email" id="email" formControlName="email">
<div *ngIf="personalDetails.get('email').invalid &&
(personalDetails.get('email').dirty || personalDetails.get('email').touched || submitted)">
<div *ngIf="personalDetails.get('email').errors?.['required']">Email is required</div>
<div *ngIf="personalDetails.get('email').errors?.['email']">Please enter a valid email</div>
</div>
</div>
</div>

<h3>Account Details</h3>
<div formGroupName="accountDetails">
<div>
<label for="username">Username</label>
<input type="text" id="username" formControlName="username">
<div *ngIf="accountDetails.get('username').invalid &&
(accountDetails.get('username').dirty || accountDetails.get('username').touched || submitted)">
<div *ngIf="accountDetails.get('username').errors?.['required']">Username is required</div>
<div *ngIf="accountDetails.get('username').errors?.['minlength']">
Username must be at least 4 characters
</div>
</div>
</div>

<div>
<label for="password">Password</label>
<input type="password" id="password" formControlName="password">
<div *ngIf="accountDetails.get('password').invalid &&
(accountDetails.get('password').dirty || accountDetails.get('password').touched || submitted)">
<div *ngIf="accountDetails.get('password').errors?.['required']">Password is required</div>
<div *ngIf="accountDetails.get('password').errors?.['minlength']">
Password must be at least 8 characters
</div>
</div>
</div>

<div>
<label for="confirmPassword">Confirm Password</label>
<input type="password" id="confirmPassword" formControlName="confirmPassword">
<div *ngIf="accountDetails.get('confirmPassword').invalid &&
(accountDetails.get('confirmPassword').dirty || accountDetails.get('confirmPassword').touched || submitted)">
<div *ngIf="accountDetails.get('confirmPassword').errors?.['required']">
Confirm Password is required
</div>
<div *ngIf="accountDetails.get('confirmPassword').errors?.['passwordMismatch']">
Passwords do not match
</div>
</div>
</div>
</div>

<div>
<input type="checkbox" id="acceptTerms" formControlName="acceptTerms">
<label for="acceptTerms">I accept the Terms and Conditions</label>
<div *ngIf="f['acceptTerms'].invalid && (f['acceptTerms'].dirty || f['acceptTerms'].touched || submitted)">
You must accept the terms and conditions
</div>
</div>

<button type="submit">Register</button>
</form>

This example demonstrates:

  • Nested form groups
  • Custom validators
  • Complex validation error handling
  • Form submission workflow

When to Choose Each Form Type

Choose Template-driven Forms when:

  • Building simple forms with basic validation
  • Working on small applications
  • Validation logic is simple
  • You prefer more HTML, less TypeScript

Choose Reactive Forms when:

  • Building complex forms with dynamic controls
  • Working with complex validation logic
  • Building large, enterprise applications
  • You need to unit test form logic
  • You need to implement dynamic form behaviors

Summary

In this introduction to Angular forms, we've covered:

  1. The two types of Angular forms and their key differences
  2. How to set up your Angular app to use forms
  3. Basic examples of both template-driven and reactive forms
  4. Form control states and validation
  5. A real-world example of a complex form
  6. Guidelines for choosing between form types

Forms are a critical part of most web applications, and Angular's form capabilities provide powerful tools for creating robust user interfaces. As you continue learning about Angular forms, you'll discover more advanced features like dynamic form controls, custom validators, and form arrays.

Further Learning

To deepen your understanding of Angular forms, try these exercises:

  1. Basic Exercise: Create a simple contact form with name, email, and message fields using template-driven forms
  2. Intermediate Exercise: Rebuild the same contact form using reactive forms, adding validation for all fields
  3. Advanced Exercise: Create a multi-step form wizard with validation at each step using reactive forms

Additional Resources

With these fundamentals, you're ready to start building forms in your Angular applications!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)