Alex Beugnet
Alex Beugnet

Reputation: 4071

Angular2 weird form behavior with *ngFor

Hey again for those who have seen my several posts about this form with the bottles... It's back again.

Before I start explaining anything, here is a working plunkr showing the issue :

Working Plunkr

N.B : I'm displaying the Bottle typeId in the 2nd array, it shows the issue clearly.


The issue :

I was checking the bindings to make sure the values I was selecting were passed correctly, when I encountered an issue with the following scenario :

Is there a way to avoid this weird behavior using standard Template Driven Forms ? Or do I have to go through ReactiveForms ?


I'll explain here most of my code :

I'm sending an Array of gas bottles (bottleArray) containing their name and typeId to my form as @Input().

I deep clone this array and its objets into two separate arrays : orderedClonedArrayBottles and returnedClonedArrayBottles.

I then add values to my corresponding displayed arrays : orderedBottles and returnedBottles which have now a count value.

...
@Input() private bottleArray: Bottle[];

private orderedClonedArrayBottles: Bottle[] = [];
private returnedClonedArrayBottles: Bottle[] = [];
private orderedBottles: BottleCommand[] = [];
private returnedBottles: BottleCommand[] = [];

ngOnChanges(changes) {
  // Get @Input data when it's ready
  if (changes.bottleArray) {
    // Cloning the Array AND the Bottles
    this.orderedClonedArrayBottles = this.deepClone(changes.bottleArray.currentValue);
    this.returnedClonedArrayBottles = this.deepClone(changes.bottleArray.currentValue);

    // Display first rows
    if (this.orderedClonedArrayBottles.length > 0) {
      this.orderedBottles.push(this.orderedClonedArrayBottles[0]);
    }
    if (this.returnedClonedArrayBottles.length > 0) {
      this.returnedBottles.push(this.returnedClonedArrayBottles[0]);
    }
  }
}

I can delete any index of any Array with the following method :

removeRow(index: number, type: string, event: Event): void {
  event.stopPropagation();
  if (type == 'order') {
    // Cleans the reference 'count' value.
    this.orderedBottles[index].count = null;
    this.orderedBottles.splice(index, 1);
  } else {
    this.returnedBottles[index].count = null;
    this.returnedBottles.splice(index, 1);
  }
}

I can push items in any of these 2 arrays with 2 separate buttons : AddOrder() and AddReturnOrder() (same code) :

addOrder(event: Event): void {
  event.stopPropagation();
  // Limits to the number of types
  if (this.orderedBottles.length < this.orderedClonedArrayBottles.length) {
    let index = this.getAvailableIndex(this.orderedBottles, this.orderedClonedArrayBottles);
    this.orderedBottles.push(this.orderedClonedArrayBottles[index]);
  }
}

To avoid pushing an existing reference in my arrays, I use the following method that will get me the first available index that is not already displayed :

/**
 * Gets the first available index from 2 arrays containing the same references.
 *
 * @param {Object[]} displayedArray Array containing the occupied indexes
 * @param {Object[]} referenceArray Array containing all the indexes
 * @return {number} Index of the first available position
 */
getAvailableIndex(displayedArray: Object[], referenceArray: Object[]): number {
  let index: number = null;
  // Gets the available indexes of Bottles by filtering the referenceArray with the displayedArray
  let availablePositions: Object[] = referenceArray.filter(element => displayedArray.indexOf(element) < 0);
  // Return the first position available
  return index = referenceArray.indexOf(availablePositions[0]);
}

Here is the corresponding HTML : (clearer in Plunkr)

  <div fxLayout="row" style="max-width: 80%">
    <div fxLayout="column" style="min-width: 50%">

      <div fxLayout="row" style="max-width: 100%" *ngFor="let bottle of orderedBottles; let i = index">
        <md-select class="select" placeholder="Select bottle type" name="orderedTypeSelect_{{i}}" [(ngModel)]="orderedBottles[i].typeId">
          <md-option class="options" *ngFor="let type of bottleArray" [value]="type.typeId">
            {{ type.name }}
          </md-option>
        </md-select>

        <md-input-container class="container">
          <input md-input type="number" name="orderedBottleInput_{{i}}" autocomplete="off" [(ngModel)]="orderedBottles[i].count"
          step="1" min="0" max="99">
        </md-input-container>

        <button class="button-row" type="button" (click)="removeRow(i, 'order', $event)">-</button>

        {{orderedBottles[i].typeId}} -
        {{orderedBottles[i].count}}
      </div>

    </div>


    <div fxLayout="column" style="min-width: 50%">

      <div fxLayout="row" style="max-width: 100%" *ngFor="let bottle of returnedBottles; let j = index">
        <md-select class="select" placeholder="Select bottle type" name="returnedTypeSelect_{{j}}" [(ngModel)]="returnedBottles[j].typeId">
          <md-option class="options" *ngFor="let type of bottleArray; let z = index" [value]="bottleArray[z].typeId">
            {{ bottleArray[z].typeId }}
          </md-option>
        </md-select>

        <md-input-container class="container">
          <input md-input type="number" name="returnedBottleInput_{{j}}" autocomplete="off" [(ngModel)]="returnedBottles[j].count"
          step="1" min="0" max="99">
        </md-input-container>

        <button style="margin-top: -20px;" class="button-row" type="button" (click)="removeRow(j, 'return', $event)">-</button>

        {{returnedBottles[j].typeId}} -
        {{returnedBottles[j].count}}
      </div>

    </div>
  </div>

  <div class="margin">
    <button md-raised-button type="button" class="submit-button" (click)="addOrder($event)">Add Order</button>
    <button md-raised-button type="button" class="submit-button" (click)="addReturnOrder($event)">Add Return Order</button>
  </div>

Upvotes: 3

Views: 934

Answers (1)

yurzui
yurzui

Reputation: 214017

Use trackBy to avoid mess:

*ngFor="let bottle of orderedBottles; let i = index; trackBy: trackByFn"

trackByFn(index) {
    return index;
}

It also improves your performance

Modified Plunker

See also

Upvotes: 3

Related Questions