Search code examples
arraysangulartypescriptangular2-changedetection

ngOnChanges only triggered on last iteration


I have the following structure:

Data-Handling Component
  |
  |-- Grid Component
  |-- Chart Component

That is Data-Handling Component is the parent of two sibling components: Grid and Chart.

I have an array of numbers which is shared between the three components via @Input() / @Output properties.

When in the Grid component I update a single value of the array, an @Output() property sends the information to the parent Data-Handling component, which in turn, sends the information to the Chart component through one of his @Input() properties.

As such (I've re-written and skipped some of the code, so some syntactic errors might be present):

grid.component.html:

[Not Relevant?] 

grid.component.ts:

@Input() gridValues: any[] = []  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

@Output() valueChanged: EventEmitter<any> = new EventEmitter<any>()

updateAllValuesToThree(): void {
    for (var i = 0; i < this.gridValues.length; i++) {
        this.updateValue(i, 3)
    }
}

updateValue(index: number, value: number): void {
    let updatedValue: any = {}

    updatedValue.index = index
    updatedValue.value = value

    this.gridValues[index] = value

    this.valueChanged.emit(updatedValue)
}

dataHandling.component.html:

<grid (valueChanged)="onValueUpdate($event)"
      [gridValues]="dataArray">
</grid>

<chart [chartData]="dataArray"
       [individualValueChanged]="individualValueChanged">
</chart>

dataHandling.component.ts:

dataArray: number[] = []  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
individualValueChanged: any = {}

onValueUpdate(event: any): void {
    this.dataArray[event.currentValue.index] = event.currentValue.value

    this.individualValueChanged = event.currentValue
}

chart.component.ts:

@Input chartData: number[] = [] // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
@Input individualValueChanged: any = {}

ngOnChanges(changes: SimpleChanges): void {
    if (changes.individualValueChanged && changes.individualValueChanged.currentValue) {
        let changeInfo: any = changes.individualValueChanged.currentValue

        this.chartData[changeInfo.index] = changeInfo.value
    }
}

chart.component.html:

[Not Relevant?]

Well. If I update a SINGLE value from the Grid to the Chart, everything's OK.

However, when I call the EventEmitter inside the for loop for every element of the gridValues array, only the last element of the array is modified in the chart component's chartData array.

So:

  • in the Grid component I'd have: [3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
  • in the Data-Handling component I'd have: [3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
  • in the Chart component I'd have: [1, 2, 3, 4, 5, 6, 7, 8, 9, 3]

Every event fires correctly into the Data-Handling component from the Grid, but only the last update event goes into the Chart component, thus triggering the ngOnChanges().

Is this working as intended? Am I missing something?


Solution

  • This is expected behavior. @Input updates are done during change detection. @Output are processed outside of change detection.

    When you trigger an event here:

    for (var i = 0; i < this.gridValues.length; i++) {
        this.updateValue(i, 3)
    }
    

    The following method in data handling component is triggered:

    onValueUpdate(event: any): void {
        this.dataArray[event.currentValue.index] = event.currentValue.value
    
        this.individualValueChanged = event.currentValue
    }
    

    So this method is triggered as many times as you trigger this.updateValue(i, 3). The last value of event.currentValue will be set for this.individualValueChanged.

    Only once all events are processed Angular goes through the change detection stage and updates the input individualValueChanged binding which contains the last value.

    For more information on change detection read Everything you need to know about change detection in Angular. Also read Two Phases of Angular Applications