Skip to main content

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 use async pipe)