3

I can't figure out how to implement React-style controlled components in Angular. The behavior I'm trying to implement is related to mat-select, but this seems to work the same way with regular selects as well. So here's what I'd like to achieve:

  • the select receives it's value via an input
  • when the select changes, the parent component will receive the change and handle it. It's the parent's business how it handles the change.
  • if the parent does not change the value of the input, I would like the select to still display the initial value, despite the user's attempted change

This is trivial to implement in React, as it is the default behavior for any sort of input. I tried to achieve a similar behavior in Angular using [ngModel] along with (ngModelChange), but it seems I'm doing it wrong.

It might seem like it's a weird behavior to ask for, but that's not the case: the selection change might trigger an async action that might succeed or fail, so it would make sense that in case of failure the value would not change. Here's an example: the update will fail half the time; when it does, the value stays the same, otherwise it changes to the new value.

1 Answer 1

0

The solution I could come up with is more like a hack, but it seems to work.

For Angular, you need to make the mat-select believe that there was an update on the value.

import { Component } from '@angular/core';
import {MatSelectModule} from '@angular/material/select';

const fruits: string[] = ['Apple', 'Banana', 'Orange'];

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  readonly fruits = fruits;
  selectedFruit = '';
  allowChanges = true;

  selectFruit(newValue: string) {
    if (this.allowChanges) {
      this.selectedFruit = newValue;
    }
    // What follows is a hack so that the mat-select thinks there was a change
    // even if there wasn't.
    // This make sure the mat-select always shows the value specified by
    // `this.selectedFruit`.
    const oldValue = this.selectedFruit;
    const anInvalidValue = '.';
    this.selectedFruit = anInvalidValue;
    setTimeout(() => {
      // This is here to prevent a race condition. If they are different, it
      // means `this.selectedFruit` was changed in the meantime.
      if (this.selectedFruit === anInvalidValue) {
        this.selectedFruit = oldValue;
      }
    }, 0);
  }
}
<section>
  <mat-form-field>
    <mat-label>Favorite Fruit</mat-label>
    <mat-select
      [ngModel]="selectedFruit" (ngModelChange)="selectFruit($event)">
      <mat-option *ngFor="let fruit of fruits" [value]="fruit">
        {{fruit}}
      </mat-option>
    </mat-select>
  </mat-form-field>
</section>
<section>
  <mat-checkbox [(ngModel)]="allowChanges">Allow changes</mat-checkbox>
</section>
<section>
  Allow Changes value: {{allowChanges}}
  <br>
  Favorit Fruit value: {{selectedFruit}}
</section>

See it in stackblitz.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.