Skip to main content

Angular Form Controls

Introduction

Form controls are the backbone of any Angular form implementation. They represent individual input elements like text fields, checkboxes, radio buttons, and select dropdowns in your forms. Angular provides two different approaches to working with forms: Template-driven forms and Reactive forms. In both approaches, form controls are fundamental building blocks.

In this tutorial, you'll learn how to create, manipulate, and validate form controls in Angular applications. We'll cover both template-driven and reactive approaches, helping you understand when to use each and how they work under the hood.

What are Form Controls?

In Angular, a form control is an object that maintains the state of an individual form element:

  • Value: The current value of the form control
  • Validation state: Whether the control is valid, invalid, dirty, touched, etc.
  • User interactions: Whether the user has changed the value or blurred the field
  • Error messages: Information about validation failures

Angular's FormControl class encapsulates this functionality, making it easy to manage form elements throughout their lifecycle.

Getting Started with Form Controls

To use form controls in Angular, you first need to import the necessary modules in your application module:

typescript
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
imports: [
// For template-driven forms
FormsModule,
// For reactive forms
ReactiveFormsModule
],
// other Angular module properties
})
export class AppModule { }

Let's look at both approaches to working with form controls.

Template-Driven Form Controls

Template-driven forms are simpler to implement but provide less explicit control. They're great for simple forms and when you're just getting started with Angular.

Basic Template-Driven Example

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

@Component({
selector: 'app-template-form',
template: `
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<div>
<label for="name">Name</label>
<input
type="text"
id="name"
name="name"
[(ngModel)]="user.name"
required
#nameInput="ngModel">

<div *ngIf="nameInput.invalid && (nameInput.dirty || nameInput.touched)">
Name is required
</div>
</div>

<div>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
[(ngModel)]="user.email"
required
email
#emailInput="ngModel">

<div *ngIf="emailInput.invalid && (emailInput.dirty || emailInput.touched)">
<div *ngIf="emailInput.errors?.['required']">Email is required</div>
<div *ngIf="emailInput.errors?.['email']">Please enter a valid email</div>
</div>
</div>

<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
`
})
export class TemplateFormComponent {
user = {
name: '',
email: ''
};

onSubmit(form: any) {
if (form.valid) {
console.log('Form submitted with values:', this.user);
}
}
}

In the template-driven approach:

  1. The ngModel directive creates and manages a form control for each input
  2. Two-way binding with [(ngModel)] synchronizes the form input with a component property
  3. Template reference variables like #nameInput="ngModel" give you access to the NgModel directive and its properties
  4. HTML5 validations and Angular-specific validators can be applied directly in the template
  5. We use ngIf directives to conditionally show validation error messages

Reactive Form Controls

Reactive forms give you more explicit control, are more testable, and scale better for complex forms. Here's how to create form controls with the reactive approach:

Basic Reactive Form Example

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

@Component({
selector: 'app-reactive-form',
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div>
<label for="name">Name</label>
<input type="text" id="name" formControlName="name">

<div *ngIf="name.invalid && (name.dirty || name.touched)">
<div *ngIf="name.errors?.['required']">Name is required</div>
<div *ngIf="name.errors?.['minlength']">Name must be at least 3 characters</div>
</div>
</div>

<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>

<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>

<div>Form Status: {{ userForm.status }}</div>
<div>Form Value: {{ userForm.value | json }}</div>
`
})
export class ReactiveFormComponent implements OnInit {
userForm!: FormGroup;

constructor(private fb: FormBuilder) {}

ngOnInit() {
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]]
});
}

get name() { return this.userForm.get('name')!; }
get email() { return this.userForm.get('email')!; }

onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted with values:', this.userForm.value);
}
}
}

In the reactive approach:

  1. Form controls are explicitly created in the component code using FormBuilder or directly with new FormControl()
  2. The formGroup directive connects the form to the component's form model
  3. The formControlName directive links individual inputs to specific form controls
  4. Validators are applied in the component code
  5. Getter methods provide convenient access to form controls for validation

Creating a Form Control Manually

You can also create form controls directly without using FormBuilder:

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

@Component({
selector: 'app-manual-form',
template: `
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<label>
First Name:
<input type="text" formControlName="firstName">
</label>

<label>
Last Name:
<input type="text" formControlName="lastName">
</label>

<button type="submit" [disabled]="profileForm.invalid">Submit</button>
</form>
`
})
export class ManualFormComponent {
profileForm = new FormGroup({
firstName: new FormControl('', Validators.required),
lastName: new FormControl('', Validators.required)
});

onSubmit() {
console.log(this.profileForm.value);
}
}

Form Control Properties and Methods

Angular's form controls provide many useful properties and methods:

Common Properties

typescript
// In your component
const nameControl = new FormControl('John');

// Properties
console.log(nameControl.value); // 'John'
console.log(nameControl.valid); // true or false
console.log(nameControl.pristine); // true if user has not interacted
console.log(nameControl.dirty); // true if user has changed value
console.log(nameControl.touched); // true if user has blurred the control
console.log(nameControl.untouched); // true if user has not blurred the control
console.log(nameControl.errors); // null or object with validation errors

Common Methods

typescript
// Setting a new value (doesn't trigger events)
nameControl.setValue('Jane');

// Patching a value (safer for forms with multiple controls)
userForm.patchValue({ name: 'Jane' });

// Reset control to initial state
nameControl.reset();

// Mark as touched (useful for showing validation on submit)
nameControl.markAsTouched();

// Disable/enable the control
nameControl.disable(); // Disables the control
nameControl.enable(); // Enables the control

Real-World Example: Dynamic Form Controls

Let's see a more complex example: a dynamic form that allows users to add multiple addresses:

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

@Component({
selector: 'app-dynamic-form',
template: `
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<div>
<label for="firstName">First Name</label>
<input id="firstName" formControlName="firstName">
</div>

<div>
<label for="lastName">Last Name</label>
<input id="lastName" formControlName="lastName">
</div>

<div formArrayName="addresses">
<h3>Addresses</h3>
<button type="button" (click)="addAddress()">Add Address</button>

<div *ngFor="let address of addresses.controls; let i = index">
<div [formGroupName]="i">
<h4>Address #{{ i + 1 }}</h4>

<div>
<label for="street-{{i}}">Street</label>
<input id="street-{{i}}" formControlName="street">
</div>

<div>
<label for="city-{{i}}">City</label>
<input id="city-{{i}}" formControlName="city">
</div>

<div>
<label for="zip-{{i}}">Zip</label>
<input id="zip-{{i}}" formControlName="zip">
</div>

<button type="button" (click)="removeAddress(i)">Remove</button>
</div>
</div>
</div>

<button type="submit" [disabled]="profileForm.invalid">Submit</button>
</form>

<div>
<h4>Form Value:</h4>
<pre>{{ profileForm.value | json }}</pre>
</div>
`
})
export class DynamicFormComponent {
profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
addresses: this.fb.array([
this.createAddressGroup()
])
});

constructor(private fb: FormBuilder) {}

get addresses(): FormArray {
return this.profileForm.get('addresses') as FormArray;
}

createAddressGroup(): FormGroup {
return this.fb.group({
street: ['', Validators.required],
city: ['', Validators.required],
zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]]
});
}

addAddress(): void {
this.addresses.push(this.createAddressGroup());
}

removeAddress(index: number): void {
this.addresses.removeAt(index);
}

onSubmit(): void {
if (this.profileForm.valid) {
console.log('Form submitted!', this.profileForm.value);
}
}
}

In this example:

  1. We use a FormArray to manage a dynamic list of address form groups
  2. Each address is its own FormGroup with street, city, and zip controls
  3. Users can add and remove addresses dynamically
  4. We display the form's current value using the JSON pipe

Custom Validators for Form Controls

Angular allows you to create custom validators for specialized validation needs:

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

// Custom validator to check if a control value matches a pattern
export function patternValidator(regex: RegExp, error: ValidationErrors): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control.value) {
// If control is empty, return no error
return null;
}

// Test the value of the control against the regex pattern
const valid = regex.test(control.value);

// If true, return null (no error), else return the error object
return valid ? null : error;
};
}

// Usage in component
this.passwordForm = this.fb.group({
password: ['', [
Validators.required,
Validators.minLength(8),
patternValidator(/\d/, { hasNumber: true }),
patternValidator(/[A-Z]/, { hasCapitalCase: true }),
patternValidator(/[a-z]/, { hasSmallCase: true }),
patternValidator(/[!@#$%^&*(),.?":{}|<>]/, { hasSpecialCharacters: true })
]]
});

Form Control Value Changes

One of the most powerful features of Angular form controls is the ability to listen to value changes and react accordingly:

typescript
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
selector: 'app-search-component',
template: `
<div>
<label for="search">Search</label>
<input id="search" [formControl]="searchControl">
</div>
<div *ngIf="loading">Loading...</div>
<ul>
<li *ngFor="let result of searchResults">{{ result }}</li>
</ul>
`
})
export class SearchComponent implements OnInit {
searchControl = new FormControl('');
searchResults: string[] = [];
loading = false;

ngOnInit() {
this.searchControl.valueChanges.pipe(
debounceTime(300), // Wait 300ms after each keystroke
distinctUntilChanged() // Only emit if value has changed
).subscribe(term => {
this.loading = true;
this.searchItems(term);
});
}

searchItems(term: string) {
// Simulate an API call
setTimeout(() => {
this.searchResults = ['Result 1 for ' + term, 'Result 2 for ' + term];
this.loading = false;
}, 500);
}
}

This example creates a search input that automatically updates search results as the user types, but waits until the user pauses typing to avoid excessive API calls.

Summary

In this tutorial, you've learned about Angular form controls, which are essential components for handling user input in your Angular applications. We've covered:

  1. The basics of form controls in both template-driven and reactive forms
  2. Creating and validating form controls
  3. Accessing and manipulating control properties
  4. Building dynamic forms with FormArray
  5. Creating custom validators
  6. Responding to value changes with observables

Understanding form controls allows you to create robust, interactive forms that provide immediate feedback to users and maintain complex state. While template-driven forms are simpler to implement, reactive forms offer more power and flexibility for complex scenarios.

Additional Resources

Practice Exercises

  1. Create a registration form with fields for username, email, password, and password confirmation. Add validation to ensure:

    • Username is at least 3 characters
    • Email is valid
    • Password is at least 8 characters with at least one number and one special character
    • Password confirmation matches the password
  2. Build a dynamic quiz form where users can add multiple questions and possible answers.

  3. Implement a form with conditional fields that appear or disappear based on other field values (for example, show a "Company Name" field only when "Employment Type" is set to "Employed").



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