Forms Implementation Skill
Quick Start
Template-Driven Forms
import { Component } from '@angular/core'; import { NgForm } from '@angular/forms';
@Component({
selector: 'app-contact',
template: <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)"> <input [(ngModel)]="model.name" name="name" required minlength="3" /> <input [(ngModel)]="model.email" name="email" email /> <button [disabled]="!contactForm.valid">Submit</button> </form>
})
export class ContactComponent {
model = { name: '', email: '' };
onSubmit(form: NgForm) { if (form.valid) { console.log('Form submitted:', form.value); } } }
Reactive Forms
import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
template: <form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="name" placeholder="Name" /> <input formControlName="email" type="email" /> <input formControlName="password" type="password" /> <button [disabled]="form.invalid">Register</button> </form>
})
export class UserFormComponent implements OnInit {
form!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() { this.form = this.fb.group({ name: ['', [Validators.required, Validators.minLength(3)]], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]] }); }
onSubmit() { if (this.form.valid) { console.log(this.form.value); } } }
Form Controls
FormControl
// Create standalone control const nameControl = new FormControl('', Validators.required);
// Get value nameControl.value
// Set value nameControl.setValue('John'); nameControl.patchValue({ name: 'John' });
// Check validity nameControl.valid nameControl.invalid nameControl.errors
// Listen to changes nameControl.valueChanges.subscribe(value => { console.log('Changed:', value); });
FormGroup
const form = new FormGroup({ name: new FormControl('', Validators.required), email: new FormControl('', [Validators.required, Validators.email]), address: new FormGroup({ street: new FormControl(''), city: new FormControl(''), zip: new FormControl('') }) });
// Access nested controls form.get('address.street')?.setValue('123 Main St');
// Update multiple values form.patchValue({ name: 'John', email: 'john@example.com' });
FormArray
const form = new FormGroup({ name: new FormControl(''), emails: new FormArray([ new FormControl(''), new FormControl('') ]) });
// Dynamic form array const emailsArray = form.get('emails') as FormArray;
// Add control emailsArray.push(new FormControl(''));
// Remove control emailsArray.removeAt(0);
// Iterate emailsArray.controls.forEach((control, index) => { // ... });
Validation
Built-in Validators
import { Validators } from '@angular/forms';
new FormControl('', [ Validators.required, Validators.minLength(3), Validators.maxLength(50), Validators.pattern(/^[a-z]/i), Validators.email, Validators.min(0), Validators.max(100) ])
Custom Validators
// Simple validator function noSpacesValidator(control: AbstractControl): ValidationErrors | null { if (control.value && control.value.includes(' ')) { return { hasSpaces: true }; } return null; }
// Cross-field validator function passwordMatchValidator(group: FormGroup): ValidationErrors | null { const password = group.get('password')?.value; const confirm = group.get('confirmPassword')?.value;
return password === confirm ? null : { passwordMismatch: true }; }
// Usage const form = new FormGroup({ username: new FormControl('', noSpacesValidator), password: new FormControl(''), confirmPassword: new FormControl('') }, passwordMatchValidator);
Async Validators
function emailAvailableValidator(service: UserService): AsyncValidatorFn { return (control: AbstractControl): Observable<ValidationErrors | null> => { if (!control.value) { return of(null); }
return service.checkEmailAvailable(control.value).pipe(
map(available => available ? null : { emailTaken: true }),
debounceTime(300),
first()
);
}; }
// Usage new FormControl('', { validators: Validators.required, asyncValidators: emailAvailableValidator(userService), updateOn: 'blur' });
Form State
const control = form.get('email')!;
// Pristine/Dirty control.pristine // Not modified by user control.dirty // Modified by user
// Touched/Untouched control.untouched // Never focused control.touched // Focused at least once
// Valid/Invalid control.valid control.invalid control.errors control.pending // Async validation in progress
// Status control.status // 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'
// Value control.value control.getRawValue() // Include disabled controls
Form Display
Showing Errors
<div *ngIf="form.get('email')?.hasError('required')"> Email is required </div>
<div *ngIf="form.get('email')?.hasError('email')"> Invalid email format </div>
<div *ngIf="form.get('email')?.hasError('emailTaken')"> Email already in use </div>
Dynamic Forms
@Component({
template: <form [formGroup]="form"> <div formArrayName="items"> <div *ngFor="let item of items.controls; let i = index"> <input [formControlName]="i" /> <button (click)="removeItem(i)">Remove</button> </div> </div> <button (click)="addItem()">Add Item</button> </form>
})
export class DynamicFormComponent {
form!: FormGroup;
get items() { return this.form.get('items') as FormArray; }
addItem() { this.items.push(new FormControl('', Validators.required)); }
removeItem(index: number) { this.items.removeAt(index); } }
Advanced Patterns
FormBuilder Groups
this.form = this.fb.group({ basicInfo: this.fb.group({ firstName: ['', Validators.required], lastName: ['', Validators.required], email: ['', [Validators.required, Validators.email]] }), address: this.fb.group({ street: [''], city: [''], zip: [''] }), preferences: this.fb.array([]) });
Directives for Template Forms
<form #form="ngForm"> <input [(ngModel)]="user.name" name="name" required minlength="3" #nameField="ngModelGroup" />
<div *ngIf="nameField.invalid && nameField.touched"> <p *ngIf="nameField.errors?.['required']">Required</p> <p *ngIf="nameField.errors?.['minlength']">Min length 3</p> </div> </form>
Testing Forms
describe('UserFormComponent', () => { let component: UserFormComponent; let fixture: ComponentFixture<UserFormComponent>;
beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UserFormComponent], imports: [ReactiveFormsModule] }).compileComponents();
fixture = TestBed.createComponent(UserFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should submit valid form', () => { component.form.patchValue({ name: 'John', email: 'john@example.com' });
expect(component.form.valid).toBe(true);
});
it('should show error on invalid email', () => { component.form.get('email')?.setValue('invalid'); expect(component.form.get('email')?.hasError('email')).toBe(true); }); });
Best Practices
-
Reactive Forms for Complex: Use for validation, computed fields
-
Template Forms for Simple: Use for simple, data-binding heavy forms
-
Always validate: Server and client validation
-
Disable submit until valid: Better UX
-
Show errors appropriately: After touched/dirty
-
Handle async validation: Debounce, cancel on unsubscribe
-
Test forms thoroughly: Validation, submission, edge cases
Resources
-
Angular Forms Guide
-
Reactive Forms
-
Form Validation