angularangular-formsangular-componentsangular-changedetectionangular-binding

Why does my Angular dropdown update selection even when the parent does not store the selected value?


I have an Angular dropdown component (CustomDropdownComponent) that receives a list of options and a selected value from its parent component via @Input(). The parent also listens for selection changes using @Output().

However, I intentionally did not update the selectedValue in the parent when handling the (selectionChange) event to see if the dropdown would revert to the default selection. Surprisingly, the dropdown still updates to the newly selected option, even though the parent state remains unchanged.

My Questions:

Here is a simplified version of my implementation:

Parent Component (app.component.ts)

export class AppComponent {
  options = [
    { value: '1', text: 'Option 1' },
    { value: '2', text: 'Option 2' },
    { value: '3', text: 'Option 3' }
  ];

  selectedValue = ''; // Intentionally not updating this when selection changes

  handleOnChange(event: any) {
    console.log(event.target.value); // Logging the value but not saving it
  }
}

Parent Component Template (app.component.html)

<app-custom-dropdown 
  [options]="options" 
  [selected]="selectedValue" 
  (selectionChange)="handleOnChange($event)">
</app-custom-dropdown>

Child Component (custom-dropdown.component.ts)

@Component({
  selector: 'app-custom-dropdown',
  templateUrl: './custom-dropdown.component.html',
  styleUrls: ['./custom-dropdown.component.scss']
})
export class CustomDropdownComponent {
  @Input() options: { value: string; text: string }[] = [];
  @Input() selected: string = '';

  @Output() selectionChange: EventEmitter<any> = new EventEmitter<any>();

  onChange(event: any) {
    this.selectionChange.emit(event); // Emitting value but parent does not save it
  }
}

Child Component Template (custom-dropdown.component.html)

<select (change)="onChange($event)" [value]="selected">
  <option value="" hidden>default</option>
  <option *ngFor="let option of options" [value]="option.value">
    {{ option.text }}
  </option>
</select>

Solution

  • My reasoning is this:

    You have a property binding [value]="selected" this means that only when the value selected has an actual value change (1 -> 2) will change detection run for the select and update the DOM, but in your scenario, you never update selected value either in the child nor the parent, so there is no need for the view to update when the input value does not change.

    If you check this below code, you will see, the HTML updated when I programatically set selectedValue on the parent to 3, but not on subsequent changes. Because the value goes from ('' --on first attempt--> 3 --on subsequent changes--> 3(remains 3)) So the HTML never updates and keeps the user changes.

    If you want to achieve a sort of revert, you have to do it programmatically.

    import { Component, EventEmitter, Input, Output } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { CommonModule } from '@angular/common';
    import { FormsModule } from '@angular/forms';
    
    @Component({
      selector: 'app-custom-dropdown',
      imports: [CommonModule, FormsModule],
      template: `
      <select [value]="selected" (change)="onChange($event)">
        <option value="" hidden>default</option>
        <option *ngFor="let option of options" [value]="option.value">
          {{ option.text }}
        </option>
      </select>
      `,
    })
    export class CustomDropdownComponent {
      @Input() options: { value: string; text: string }[] = [];
      @Input() selected: string = '';
    
      @Output() selectionChange: EventEmitter<any> = new EventEmitter<any>();
    
      onChange(event: any) {
        this.selectionChange.emit(event); // Emitting value but parent does not save it
      }
    
      ngDoCheck() {
        console.log('CustomDropdownComponent check');
      }
    }
    
    @Component({
      selector: 'app-root',
      imports: [CustomDropdownComponent],
      template: `
        <app-custom-dropdown 
          [options]="options" 
          [selected]="selectedValue" 
          (selectionChange)="handleOnChange($event)">
        </app-custom-dropdown>
      `,
    })
    export class App {
      options = [
        { value: '1', text: 'Option 1' },
        { value: '2', text: 'Option 2' },
        { value: '3', text: 'Option 3' },
      ];
    
      ngDoCheck() {
        console.log('App check');
      }
    
      selectedValue = ''; // Intentionally not updating this when selection changes
    
      handleOnChange(event: any) {
        this.selectedValue = '3';
        console.log(event.target.value); // Logging the value but not saving it
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo