Angular <select> Bound to Object Confirm Change or Revert to the Previous Value
Asked Answered
R

1

6

I'm trying to achieve pretty simple <select> behavior: revert to the previous value, if user canceled the change. Actually, I succeeded, but that cost me few hours, and I'm still unhappy with the implementation, because it's not so obvious and tricky.

So, here's the template:

<select id="states" class="form-control" name="states" [ngModel]="selectedState"
        (ngModelChange)="onStateChange(selectedState, $event)">
    <option [ngValue]="null">All</option>
    <option *ngFor="let state of states" [ngValue]="state">{{state.name}}</option>
</select>

and component:

export class AppComponent {
  selectedState: State = null;

  states: State[] = [
    { name: 'Alabama', population: 100000 },
    { name: 'Alaska', population: 50000 }
  ];

  onStateChange(previousState: State, state: State): void {
    // If we're changing state from "All" to any, it's OK
    if (previousState === null) {
      this.selectedState = state;
      return;
    }
    // Otherwise we want the user to confirm that change
    if (confirm('Are you sure you want to select another state?')) {
      this.selectedState = state;
    } else {
      // But instead of just `this.selectedState = previousState;`
      // I need to proceed with below dirty hack.
      // Step 1: Changing property value, which is bound to `[ngModel]`,
      // to any, except for the current one, so `null` is OK
      this.selectedState = null;
      // Step 2: Reverting this property value to the previous one,
      // which is, ridiculously, already set as the previous one,
      // because we're reverting `this.selectedState`,
      // while passing exactly the same `this.selectedState`
      // to this method as a `previousState` parameter,
      // so we're actually doing `this.selectedState = this.selectedState;`,
      // but in not-so-obvious form.
      // This works only kind of asynchronously,
      // after call stack is clear.
      setTimeout(() => {
        this.selectedState = previousState;
      }, 0);
    }
  }
}

I hope that comments are self-explanatory, at least I tried to write them so they be such.

As I already mentioned in comments, simple this.selectedState = previousState; doesn't work, as well as omitting the setTimeout(). I also tried this, but got no luck:

this.selectedState = previousState;
this.changeDetectorRef.detectChanges();

My solution is based on this answer, and yes, I saw this one as well, but it just doesn't work for me, because I'm binding <select> to an object, not scalar value.

Demo: https://angular-hkaznb.stackblitz.io

Package versions:

Angular: 6.0.0

Remittee answered 6/7, 2018 at 11:25 Comment(0)
R
6

Seems like I've found more elegant solution, it's based on this answer. The idea is to manipulate <select>'s selectedIndex. So, here's the template:

<form class="form-inline mt-3">
    <div class="container-fluid">
        <div class="row">
            <div class="form-group col-12">
                <label for="states" class="mr-3">State</label>
                <select #s id="states" class="form-control" name="states" [ngModel]="selectedState"
                        (ngModelChange)="onStateChange(selectedState, $event, s)">
                    <option [ngValue]="null">All</option>
                    <option *ngFor="let state of states" [ngValue]="state">{{state.name}}</option>
                </select>
            </div>
        </div>
    </div>
</form>

and component:

onStateChange(previousState: State, state: State, statesEl: HTMLSelectElement): void {
  // If we're changing state from "All" to any, it's OK
  if (previousState === null) {
    this.selectedState = state;
    return;
  }
  // Otherwise we want the user to confirm that change
  if (confirm('Are you sure you want to select another state?')) {
    this.selectedState = state;
  } else {
    statesEl.selectedIndex = this.states.indexOf(previousState) + 1;
  }
}

+1 is needed because null is out of this.states, while being states <select>'s first <option>.

Demo: https://angular-rwnksz.stackblitz.io

Remittee answered 9/7, 2018 at 9:51 Comment(1)
Thank you for this. For whatever reason, I couldn't use the indexOf(previousState) + 1 approach when reverting to the previous selection, so I added an Index property to my array objects. When resetting the selectedIndex, instead of looking for indices, I found the previously-selected item and used its literal Index property; this helped me overcome the "+1" requirement (and the rest of the issues with my code not working).Pillow

© 2022 - 2024 — McMap. All rights reserved.