Angular Forms
Angular provides two approaches to handling user input through forms: template-driven forms and reactive forms. Both capture user input events, validate input, and create a form model.
Template-Driven Forms
Template-driven forms rely on directives in the template to create and manipulate the form model. They are simpler and work well for basic forms.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-template-form',
standalone: true,
imports: [FormsModule],
template: `
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<div>
<label>Name:
<input
type="text"
name="name"
[(ngModel)]="user.name"
required
minlength="3"
#name="ngModel"
/>
</label>
<div *ngIf="name.invalid && name.touched">
<small *ngIf="name.errors?.['required']">Name is required</small>
<small *ngIf="name.errors?.['minlength']">
Name must be at least 3 characters
</small>
</div>
</div>
<div>
<label>Email:
<input
type="email"
name="email"
[(ngModel)]="user.email"
required
email
#email="ngModel"
/>
</label>
<div *ngIf="email.invalid && email.touched">
<small *ngIf="email.errors?.['required']">Email is required</small>
<small *ngIf="email.errors?.['email']">Invalid email format</small>
</div>
</div>
<div>
<label>Age:
<input
type="number"
name="age"
[(ngModel)]="user.age"
required
min="18"
max="100"
/>
</label>
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
<div *ngIf="submitted">
<h3>Submitted Data:</h3>
<pre>{{ user | json }}</pre>
</div>
`
})
export class TemplateFormComponent {
user = {
name: '',
email: '',
age: null
};
submitted = false;
onSubmit(form: any) {
if (form.valid) {
console.log('Form submitted:', this.user);
this.submitted = true;
}
}
}Reactive Forms
Reactive forms provide a model-driven approach to handling form inputs. They offer more robust validation, better testability, and are more suitable for complex forms.
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-reactive-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div>
<label>Name:
<input type="text" formControlName="name" />
</label>
<div *ngIf="name?.invalid && name?.touched">
<small *ngIf="name?.errors?.['required']">Name is required</small>
<small *ngIf="name?.errors?.['minlength']">
Name must be at least 3 characters
</small>
</div>
</div>
<div>
<label>Email:
<input type="email" formControlName="email" />
</label>
<div *ngIf="email?.invalid && email?.touched">
<small *ngIf="email?.errors?.['required']">Email is required</small>
<small *ngIf="email?.errors?.['email']">Invalid email</small>
</div>
</div>
<div>
<label>Password:
<input type="password" formControlName="password" />
</label>
<div *ngIf="password?.invalid && password?.touched">
<small *ngIf="password?.errors?.['required']">Password is required</small>
<small *ngIf="password?.errors?.['minlength']">
Password must be at least 8 characters
</small>
</div>
</div>
<div formGroupName="address">
<h3>Address</h3>
<label>Street:
<input type="text" formControlName="street" />
</label>
<label>City:
<input type="text" formControlName="city" />
</label>
<label>Zip:
<input type="text" formControlName="zip" />
</label>
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
<div>
<p>Form Status: {{ userForm.status }}</p>
<p>Form Value: {{ userForm.value | json }}</p>
</div>
`
})
export class ReactiveFormComponent implements OnInit {
userForm!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
// Using FormBuilder (recommended)
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
address: this.fb.group({
street: [''],
city: ['', Validators.required],
zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]]
})
});
// Or using FormControl directly
// this.userForm = new FormGroup({
// name: new FormControl('', [Validators.required]),
// email: new FormControl('', [Validators.required, Validators.email])
// });
}
// Getters for easy access in template
get name() { return this.userForm.get('name'); }
get email() { return this.userForm.get('email'); }
get password() { return this.userForm.get('password'); }
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
// Reset form
this.userForm.reset();
} else {
// Mark all as touched to show validation errors
this.userForm.markAllAsTouched();
}
}
}Custom Validators
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// Sync validator
export function forbiddenNameValidator(forbiddenName: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = forbiddenName.test(control.value);
return forbidden ? { forbiddenName: { value: control.value } } : null;
};
}
// Password match validator
export function passwordMatchValidator(): ValidatorFn {
return (formGroup: AbstractControl): ValidationErrors | null => {
const password = formGroup.get('password');
const confirmPassword = formGroup.get('confirmPassword');
if (!password || !confirmPassword) {
return null;
}
return password.value === confirmPassword.value
? null
: { passwordMismatch: true };
};
}
// Async validator (e.g., check username availability)
export function usernameAvailableValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) {
return of(null);
}
return userService.checkUsername(control.value).pipe(
map(available => available ? null : { usernameTaken: true }),
catchError(() => of(null))
);
};
}
// Usage
this.userForm = this.fb.group({
username: ['',
[Validators.required, forbiddenNameValidator(/admin/i)],
[usernameAvailableValidator(this.userService)] // async
],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}, {
validators: passwordMatchValidator() // form-level validator
});Dynamic Forms & FormArray
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
@Component({
selector: 'app-dynamic-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div formArrayName="skills">
<h3>Skills</h3>
<div *ngFor="let skill of skills.controls; let i = index" [formGroupName]="i">
<input formControlName="name" placeholder="Skill name" />
<input formControlName="years" type="number" placeholder="Years" />
<button type="button" (click)="removeSkill(i)">Remove</button>
</div>
<button type="button" (click)="addSkill()">Add Skill</button>
</div>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
<pre>{{ form.value | json }}</pre>
`
})
export class DynamicFormComponent implements OnInit {
form!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.form = this.fb.group({
name: ['', Validators.required],
skills: this.fb.array([
this.createSkill()
])
});
}
get skills(): FormArray {
return this.form.get('skills') as FormArray;
}
createSkill(): FormGroup {
return this.fb.group({
name: ['', Validators.required],
years: [0, [Validators.required, Validators.min(0)]]
});
}
addSkill() {
this.skills.push(this.createSkill());
}
removeSkill(index: number) {
this.skills.removeAt(index);
}
onSubmit() {
if (this.form.valid) {
console.log('Submitted:', this.form.value);
}
}
}Form State & Validation
// Form control states
const control = this.form.get('email');
// Value
console.log(control?.value);
// Validity
console.log(control?.valid); // true if valid
console.log(control?.invalid); // true if invalid
console.log(control?.errors); // validation errors object
// Touch state
console.log(control?.touched); // true after blur
console.log(control?.untouched); // opposite of touched
console.log(control?.dirty); // true after value change
console.log(control?.pristine); // opposite of dirty
// Programmatic updates
control?.setValue('new value'); // Set value
control?.patchValue('partial'); // Patch value
control?.markAsTouched(); // Mark as touched
control?.markAsDirty(); // Mark as dirty
control?.updateValueAndValidity(); // Recalculate validity
// Listen to value changes
control?.valueChanges.subscribe(value => {
console.log('Value changed:', value);
});
// Listen to status changes
control?.statusChanges.subscribe(status => {
console.log('Status changed:', status); // 'VALID', 'INVALID', 'PENDING'
});
// Disable/Enable
control?.disable();
control?.enable();
// Reset
this.form.reset();
this.form.reset({ email: 'default@example.com' });
// Patch form
this.form.patchValue({
name: 'John',
email: 'john@example.com'
});
// Set entire form value
this.form.setValue({
name: 'John',
email: 'john@example.com',
address: {
street: '123 Main St',
city: 'NYC',
zip: '10001'
}
});Template-Driven vs Reactive Forms
**Template-Driven**: Good for simple forms, less code, easier to understand. **Reactive**: Better for complex forms, easier to test, more control, dynamic validation, better for large-scale apps.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free