Advanced Form Techniques
Reactive Change Listening
1@Component({...})
2export class ReactiveFormComponent implements OnInit {
3 private fb = inject(FormBuilder);
4 private destroyRef = inject(DestroyRef);
5
6 form = this.fb.group({
7 searchQuery: [''],
8 category: ['all'],
9 priceRange: this.fb.group({
10 min: [0],
11 max: [1000]
12 })
13 });
14
15 ngOnInit(): void {
16 // Listen to changes on a single field
17 this.form.get('searchQuery')?.valueChanges.pipe(
18 debounceTime(300),
19 distinctUntilChanged(),
20 takeUntilDestroyed(this.destroyRef)
21 ).subscribe(query => {
22 console.log('Searching:', query);
23 });
24
25 // Listen to changes on the entire form
26 this.form.valueChanges.pipe(
27 takeUntilDestroyed(this.destroyRef)
28 ).subscribe(formValue => {
29 console.log('Form changed:', formValue);
30 });
31
32 // Listen to status (VALID, INVALID, PENDING)
33 this.form.statusChanges.pipe(
34 takeUntilDestroyed(this.destroyRef)
35 ).subscribe(status => {
36 console.log('Status:', status);
37 });
38 }
39}
Conditional Validators
1@Component({...})
2export class ConditionalFormComponent implements OnInit {
3 form = this.fb.group({
4 contactMethod: ['email'],
5 email: [''],
6 phone: ['']
7 });
8
9 ngOnInit(): void {
10 this.form.get('contactMethod')?.valueChanges.pipe(
11 takeUntilDestroyed(this.destroyRef)
12 ).subscribe(method => {
13 const emailControl = this.form.get('email')!;
14 const phoneControl = this.form.get('phone')!;
15
16 if (method === 'email') {
17 emailControl.setValidators([Validators.required, Validators.email]);
18 phoneControl.clearValidators();
19 } else {
20 phoneControl.setValidators([Validators.required, Validators.pattern(/^\d{9}$/)]);
21 emailControl.clearValidators();
22 }
23
24 // Important! Update status after changing validators
25 emailControl.updateValueAndValidity();
26 phoneControl.updateValueAndValidity();
27 });
28 }
29}
Forms with Signals (Angular 17+)
1@Component({
2 template: \`
3 <form [formGroup]="form" (ngSubmit)="onSubmit()">
4 <input formControlName="name">
5 <p>Name: {{ nameSignal() }}</p>
6 <p>Form valid: {{ isValid() }}</p>
7 </form>
8 \`
9})
10export class SignalFormComponent {
11 private fb = inject(FormBuilder);
12
13 form = this.fb.group({
14 name: ['', Validators.required]
15 });
16
17 // Conversion to Signal
18 nameSignal = toSignal(
19 this.form.get('name')!.valueChanges,
20 { initialValue: '' }
21 );
22
23 isValid = toSignal(
24 this.form.statusChanges.pipe(map(s => s === 'VALID')),
25 { initialValue: false }
26 );
27}
Custom ControlValueAccessor
1import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
2
3@Component({
4 selector: 'app-star-rating',
5 standalone: true,
6 template: \`
7 @for (star of stars; track $index; let i = $index) {
8 <span
9 (click)="setRating(i + 1)"
10 [class.filled]="i < value">
11 ★
12 </span>
13 }
14 \`,
15 providers: [{
16 provide: NG_VALUE_ACCESSOR,
17 useExisting: StarRatingComponent,
18 multi: true
19 }]
20})
21export class StarRatingComponent implements ControlValueAccessor {
22 stars = [1, 2, 3, 4, 5];
23 value = 0;
24
25 private onChange: (value: number) => void = () => {};
26 private onTouched: () => void = () => {};
27
28 writeValue(value: number): void {
29 this.value = value || 0;
30 }
31
32 registerOnChange(fn: (value: number) => void): void {
33 this.onChange = fn;
34 }
35
36 registerOnTouched(fn: () => void): void {
37 this.onTouched = fn;
38 }
39
40 setRating(rating: number): void {
41 this.value = rating;
42 this.onChange(rating);
43 this.onTouched();
44 }
45}
46
47// Usage
48<app-star-rating formControlName="rating"></app-star-rating>