--- name: forms-implementation description: Build reactive and template-driven forms, implement custom validators, create async validators, add cross-field validation, and generate dynamic forms for Angular applications. --- # Forms Implementation Skill ## Quick Start ### Template-Driven Forms ```typescript import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; @Component({ selector: 'app-contact', template: `
` }) export class ContactComponent { model = { name: '', email: '' }; onSubmit(form: NgForm) { if (form.valid) { console.log('Form submitted:', form.value); } } } ``` ### Reactive Forms ```typescript import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-user-form', template: `
` }) 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 ```typescript // 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript // 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 ```typescript function emailAvailableValidator(service: UserService): AsyncValidatorFn { return (control: AbstractControl): Observable => { 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 ```typescript 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 ```typescript
Email is required
Invalid email format
Email already in use
``` ### Dynamic Forms ```typescript @Component({ template: `
` }) 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 ```typescript 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 ```typescript

Required

Min length 3

``` ## Testing Forms ```typescript describe('UserFormComponent', () => { let component: UserFormComponent; let fixture: ComponentFixture; 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 1. **Reactive Forms for Complex**: Use for validation, computed fields 2. **Template Forms for Simple**: Use for simple, data-binding heavy forms 3. **Always validate**: Server and client validation 4. **Disable submit until valid**: Better UX 5. **Show errors appropriately**: After touched/dirty 6. **Handle async validation**: Debounce, cancel on unsubscribe 7. **Test forms thoroughly**: Validation, submission, edge cases ## Resources - [Angular Forms Guide](https://angular.io/guide/forms) - [Reactive Forms](https://angular.io/guide/reactive-forms) - [Form Validation](https://angular.io/guide/form-validation)