1

The following code generates six <select> tags

<span *ngFor="let key of orderedKeys; let i=index;">
                Position {{i + 1}}:
                <select (change)="changePosition($event.target.value, i+1)" >
                    <option *ngFor="let option of options" [value]="option" [selected]="option === object[key].name">{{option}}</option>
                </select>
                <hr>
</span>

The function changePosition swaps the position of two keys in orderedKeys so that the selected option should change for two select tags but one select which should change the value is only focused but is not changing the selected value but why? I also tried it with [(ngModel)]=object[key].name but the same behavior is observed.

Amer
  • 6,162
  • 2
  • 8
  • 34
Unistack
  • 127
  • 7

1 Answers1

2

It's all about angular change detection... (when nested structural directives combined with conditions and user changes). In such cases we can force update conditions on next browser MacroTask:

<span *ngFor="let key of orderedKeys; let i=index;">
   Position {{i + 1}}:
   <select (change)="changePosition($event.target.value, i+1)" >
     <option *ngFor="let option of options" [value]="option" [selected]="flag && (option === object[key].name)">{{option}}</option>
     </select>
     <hr>
</span>

ts:

orderedKeys = ['keyA', 'keyB', 'keyC', 'keyD'];
  object = {
    'keyA': {name: 'nameA'},
    'keyB': {name: 'nameB'},
    'keyC': {name: 'nameC'},
    'keyD': {name: 'nameD'},
  }
  options = ['nameA', 'nameB', 'nameC', 'nameD'];
  flag = true;

  changePosition(val: string, pos: number) {
    let selectedKey = Object.keys(this.object).find(key => this.object[key].name === val);
    let keyToSwap = this.orderedKeys[pos - 1];
    
    let newOrderedKeys = [...this.orderedKeys];
    newOrderedKeys[pos - 1] = selectedKey;
    newOrderedKeys[this.orderedKeys.indexOf(selectedKey)] = keyToSwap;
    this.orderedKeys = newOrderedKeys;
    
    //force update on next browser MacroTask
    this.flag = false;
    setTimeout(() => {
      this.flag = true;
    }, 0);
  }

finally in these priority or ranking cases other implementations may be better:

  • reactive forms with FormArray (removeAt and insert fuctions)
  • other Ui components like CDK drag and drop

Update1- Reactive Forms solution

ReactiveFormsModule must be imported in your module.

template:

<form [formGroup]="formGroup">
  <div formArrayName="selects">
    <div *ngFor="let ctrl of controlsArray; let i=index;" [formGroupName]="i">
      Position {{i + 1}}:
      <select (change)="refreshOrder(i+1)" formControlName="name">
        <option *ngFor="let option of options" [value]="option">{{option}}</option>
      </select>
      <button *ngIf="i>0" (click)="move(i, -1)">move up</button>
      <button *ngIf="i<controlsArray.length-1" (click)="move(i, 1)">move down</button>
      <hr>
    </div>
  </div>
</form>

ts:

  //parent form group
  formGroup: FormGroup;

  //getter functions
  get controlsArray() {
    return this.selectsFormArray.controls;
  }
  get selectsFormArray() {
    return this.formGroup.get('selects') as FormArray
  }

  constructor() {
    let formArray = new FormArray([]);
    //create form array based on ordered keys array
    this.orderedKeys.forEach(key => {
      formArray.insert(formArray.length, new FormGroup({
        name: new FormControl(this.object[key].name)
      }))
    });

    this.formGroup = new FormGroup({selects: formArray});
  }

  refreshOrder(pos: number) {
    let val = this.controlsArray[pos-1].get('name')?.value;
    
    if (!val) return;
    let selectedKey = Object.keys(this.object).find((key: string) => this.object[key].name === val);
    let keyToSwap = this.orderedKeys[pos - 1];
    
    if(!selectedKey || !keyToSwap)
    return;

    //updating ordered keys array
    let selectedKeyPrevIndex = this.orderedKeys.indexOf(selectedKey);
    let newOrderedKeys = [...this.orderedKeys];
    newOrderedKeys[pos - 1] = selectedKey;
    newOrderedKeys[selectedKeyPrevIndex] = keyToSwap;
    this.orderedKeys = newOrderedKeys;
    
    //refresh form control
    this.controlsArray[selectedKeyPrevIndex].get('name')?.setValue(this.object[keyToSwap].name);
  }

  move(index: number, direction: number) {
    let temp = this.selectsFormArray.at(index);
    this.selectsFormArray.removeAt(index);
    this.selectsFormArray.insert(index+direction,temp);

    //updating ordered keys array too:
    let clonedOrderedKeys = [...this.orderedKeys];
    clonedOrderedKeys[index] = this.orderedKeys[index+direction];
    clonedOrderedKeys[index+direction] = this.orderedKeys[index];
    this.orderedKeys = clonedOrderedKeys;
  }

FormArray is one of the three fundamental building blocks used to define forms in Angular, along with FormControl and FormGroup and it can be an array of FormControl, FormGroup or FormArray instances.This way you could have very complicated and nested forms...

Mostafa
  • 61
  • 6
  • Thank you. Could you provide an example for reactive forms? How would I do it with FormArray? – Unistack Sep 24 '22 at 10:44
  • Yes of course, sorry for being late. The answer will be updated soon. – Mostafa Sep 25 '22 at 08:51
  • Move up/down buttons added to show other FormArray methods; Also you could have Delete button to remove a form control or an Add button to add new key selectors dynamically. – Mostafa Sep 25 '22 at 12:22