catsafety
catsafety

Reputation: 305

React-style controlled components (bound input) with Angular

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:

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.

Upvotes: 3

Views: 362

Answers (1)

epere4
epere4

Reputation: 3206

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.

Upvotes: 0

Related Questions