Forms
Template-driven 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)">
<input
name="username"
[(ngModel)]="user.username"
required
#username="ngModel">
<div *ngIf="username.invalid && username.touched">
Username required
</div>
<input
type="email"
name="email"
[(ngModel)]="user.email"
required
email>
<button [disabled]="userForm.invalid">Submit</button>
</form>
`
})
export class TemplateFormComponent {
user = { username: '', email: '' };
onSubmit(form: any) {
console.log(form.value);
}
}
Reactive Forms
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-reactive-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<input formControlName="username">
<div *ngIf="username?.invalid && username?.touched">
Username required (min 3 chars)
</div>
<input formControlName="email" type="email">
<div *ngIf="email?.errors?.['required']">Email required</div>
<div *ngIf="email?.errors?.['email']">Invalid email</div>
<button [disabled]="userForm.invalid">Submit</button>
</form>
`
})
export class ReactiveFormComponent {
fb = inject(FormBuilder);
userForm = this.fb.group({
username: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]]
});
get username() { return this.userForm.get('username'); }
get email() { return this.userForm.get('email'); }
onSubmit() {
if (this.userForm.valid) {
console.log(this.userForm.value);
}
}
}
FormControl & FormGroup
import { FormControl, FormGroup } from '@angular/forms';
// Individual control
nameControl = new FormControl('', Validators.required);
// Form group
userForm = new FormGroup({
name: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.email]),
age: new FormControl(0, [Validators.min(18), Validators.max(99)])
});
// Get values
this.userForm.value; // { name: '...', email: '...', age: 0 }
this.userForm.get('name')?.value;
// Set values
this.userForm.setValue({ name: 'John', email: 'john@test.com', age: 25 });
this.userForm.patchValue({ name: 'Jane' }); // Partial update
// Validation
this.userForm.valid;
this.userForm.invalid;
this.userForm.errors;
FormArray
import { FormArray, FormControl } from '@angular/forms';
@Component({
template: `
<form [formGroup]="form">
<div formArrayName="skills">
<div *ngFor="let skill of skills.controls; let i = index">
<input [formControlName]="i">
<button (click)="removeSkill(i)">Remove</button>
</div>
</div>
<button (click)="addSkill()">Add Skill</button>
</form>
`
})
export class FormArrayComponent {
form = new FormGroup({
skills: new FormArray([
new FormControl('')
])
});
get skills() {
return this.form.get('skills') as FormArray;
}
addSkill() {
this.skills.push(new FormControl(''));
}
removeSkill(index: number) {
this.skills.removeAt(index);
}
}
Custom Validators
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// Synchronous validator
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? { forbiddenName: { value: control.value } } : null;
};
}
// Usage
username: ['', [Validators.required, forbiddenNameValidator(/admin/)]]
// Async validator
export function uniqueEmailValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return userService.checkEmail(control.value).pipe(
map(exists => exists ? { emailExists: true } : null)
);
};
}
Value Changes
// Watch form changes
this.userForm.valueChanges.subscribe(value => {
console.log('Form changed:', value);
});
// Watch specific field
this.userForm.get('email')?.valueChanges.subscribe(email => {
console.log('Email changed:', email);
});
// Debounce
this.userForm.get('search')?.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(searchTerm => {
this.search(searchTerm);
});
Best Practices
- Use Reactive Forms for complex forms
- Use Template-driven for simple forms
- Always validate on both client and server
- Use FormBuilder for cleaner code
- Unsubscribe from
valueChanges(or useasyncpipe)