angulartypescriptangular-reactive-formsangular-formsangular19

SetValue is not triggering Dropdown's (change) function


In Angular 19, I need a function to run when a dropdown changes value. However, it doesn't happen when using setValue.

html

<select id="dropdown" [formControl]="dropdown" (change)="changeFunction()">
    {{options....}
</select>

ts

ngOnInit(){
    this.form.addControl('dropdown', new FormControl('0'))
    if(autofill === true)
        ddControler.setValue('1')   // <<<< DOES NOT RUN changeFunction()
}

get ddControler(): FormControl<string|null> { return this.form.get('dropdown') as FormControl; }

changeFunction(){
    console.log('dropdown changed')
}

Solution

  • The change event is fired only when the user interacts with the page, instead we can use valueChanges, when you specifically want the event to fire everytime the field is updated.

    Here I use pairwise to provide the old value and new value to perform an equality check to prevent extra calls and takeUntilDestroy to destroy the listener on component destroy.

    ngOnInit() {
      this.form.addControl('dropdown', new FormControl('0'));
      this.initializeListener();
      if (this.autofill) {
        this.ddControler.setValue('1');
      }
    }
    
    initializeListener() {
      this.ddControler.valueChanges
        .pipe(takeUntilDestroyed(this.destroyRef), pairwise())
        // we use pairwise to fire the change event only when the old value differs from the new value
        .subscribe((value: any[]) => {
          if (value[0] !== value[1]) {
            this.changeFunction();
          }
        });
    }
    

    Full Code:

    import { Component, DestroyRef, inject } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
    import { pairwise } from 'rxjs';
    import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
    @Component({
      selector: 'app-root',
      imports: [ReactiveFormsModule],
      template: `
        <form [formGroup]="form">
          <select id="dropdown" formControlName="dropdown">
            <option value="0">0</option>
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4">4</option>
          </select>
        </form>
      `,
    })
    export class App {
      destroyRef = inject(DestroyRef);
      autofill = true;
      form = new FormGroup({});
      ngOnInit() {
        this.form.addControl('dropdown', new FormControl('0'));
        this.initializeListener();
        if (this.autofill) {
          this.ddControler.setValue('1');
        }
      }
    
      initializeListener() {
        this.ddControler.valueChanges
          .pipe(takeUntilDestroyed(this.destroyRef), pairwise())
          // we use pairwise to fire the change event only when the old value differs from the new value
          .subscribe((value: any[]) => {
            if (value[0] !== value[1]) {
              this.changeFunction();
            }
          });
      }
    
      get ddControler(): FormControl<string | null> {
        return this.form.get('dropdown') as never as FormControl;
      }
    
      changeFunction() {
        console.log('dropdown changed');
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo

    It is ok to use the change event for simple components, that do not require the change event to fire on patch/set value events.

    For those scenarios just call the change event manually on each update.

    ngOnInit() {
      this.form.addControl('dropdown', new FormControl('0'));
      if (this.autofill) {
        this.ddControler.setValue('1');
        this.changeFunction();
      }
    }