Forms
Template-driven Forms
Section titled “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
Section titled “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
Section titled “FormControl & FormGroup”import { FormControl, FormGroup } from '@angular/forms';
// Individual controlnameControl = new FormControl('', Validators.required);
// Form groupuserForm = 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 valuesthis.userForm.value; // { name: '...', email: '...', age: 0 }this.userForm.get('name')?.value;
// Set valuesthis.userForm.setValue({ name: 'John', email: 'john@test.com', age: 25 });this.userForm.patchValue({ name: 'Jane' }); // Partial update
// Validationthis.userForm.valid;this.userForm.invalid;this.userForm.errors;FormArray
Section titled “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); }}Validation
Section titled “Validation”Custom Validators
Section titled “Custom Validators”import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// Synchronous validatorexport function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const forbidden = nameRe.test(control.value); return forbidden ? { forbiddenName: { value: control.value } } : null; };}
// Usageusername: ['', [Validators.required, forbiddenNameValidator(/admin/)]]Async Validators
Section titled “Async Validators”// Async validatorexport function uniqueEmailValidator(userService: UserService): AsyncValidatorFn { return (control: AbstractControl): Observable<ValidationErrors | null> => { return userService.checkEmail(control.value).pipe( map(exists => exists ? { emailExists: true } : null) ); };}Custom Form Controls
Section titled “Custom Form Controls”Implement ControlValueAccessor to create reusable form controls:
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({ selector: 'app-toggle-switch', template: `<input type="checkbox" [checked]="value" (change)="onChange($event.target.checked)">`, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: ToggleSwitchComponent, multi: true }]})export class ToggleSwitchComponent implements ControlValueAccessor { value = false; onChange = (_: boolean) => {}; onTouched = () => {};
writeValue(val: boolean) { this.value = val; } registerOnChange(fn: (_: boolean) => void) { this.onChange = fn; } registerOnTouched(fn: () => void) { this.onTouched = fn; }}Value Changes
Section titled “Value Changes”// Watch form changesthis.userForm.valueChanges.subscribe(value => { console.log('Form changed:', value);});
// Watch specific fieldthis.userForm.get('email')?.valueChanges.subscribe(email => { console.log('Email changed:', email);});
// Debouncethis.userForm.get('search')?.valueChanges.pipe( debounceTime(300), distinctUntilChanged()).subscribe(searchTerm => { this.search(searchTerm);});Best Practices
Section titled “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)

