We use cookies to enhance your experience on the site
CodeWorlds

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">
1112      </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>
Go to CodeWorlds