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:
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
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:
- The
ngModel
directive creates and manages a form control for each input - Two-way binding with
[(ngModel)]
synchronizes the form input with a component property - Template reference variables like
#nameInput="ngModel"
give you access to theNgModel
directive and its properties - HTML5 validations and Angular-specific validators can be applied directly in the template
- 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
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:
- Form controls are explicitly created in the component code using
FormBuilder
or directly withnew FormControl()
- The
formGroup
directive connects the form to the component's form model - The
formControlName
directive links individual inputs to specific form controls - Validators are applied in the component code
- 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
:
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
// 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
// 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:
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:
- We use a
FormArray
to manage a dynamic list of address form groups - Each address is its own
FormGroup
with street, city, and zip controls - Users can add and remove addresses dynamically
- 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:
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:
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:
- The basics of form controls in both template-driven and reactive forms
- Creating and validating form controls
- Accessing and manipulating control properties
- Building dynamic forms with FormArray
- Creating custom validators
- 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
- Angular Official Documentation on Forms
- Angular Reactive Forms Guide
- Angular Template-driven Forms Guide
Practice Exercises
-
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
-
Build a dynamic quiz form where users can add multiple questions and possible answers.
-
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! :)