My view does not update when i change my array in *ngFor

I'm trying to create some expansion panels with mat-table's inside, my problem is i have to resize my windows before my view will change. My data is loading fine and all but somehow my view does not update. My view where my expansion panels should be, is just all blank. Untill i click a button or resize my window. What can cause something like this?

In my ngOnInit() i call

this.getSale1(); 

.HTML:

<mat-accordion>
    <mat-expansion-panel *ngFor="let data of mySaleModelArray2 ">
        <mat-expansion-panel-header>
            <mat-panel-title>
                <h6 class="salepanelheadtext">Bar:</h6>{{data.name}}
            </mat-panel-title>
        <mat-panel-description>
            <h6 class="salepanelheadtext2">Total:</h6> {{data.total_sales}}
        </mat-panel-description>
        </mat-expansion-panel-header>
            <div class="example-container mat-elevation-z8">                                
                <mat-table #table [dataSource]="data.sales"  >
                <!-- PLU Column -->                                 
                <ng-container matColumnDef="pluNo">
                    <mat-header-cell *matHeaderCellDef >
                        #
                    </mat-header-cell>
                    <mat-cell *matCellDef="let salesdata"> 
                        {{salesdata.beerline}} 
                    </mat-cell>
                </ng-container>
                <!-- Name Column -->
                <ng-container matColumnDef="name">
                    <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
                    <mat-cell *matCellDef="let salesdata"> 
                         {{salesdata.pluName}} 
                    </mat-cell>
                </ng-container>
                <!-- Sold_Count Column -->
                <ng-container matColumnDef="sold_count">
                    <mat-header-cell *matHeaderCellDef> 
                        QTY 
                    </mat-header-cell>
                    <mat-cell *matCellDef="let salesdata"> 
                        {{salesdata.sold_count}} 
                    </mat-cell>
                </ng-container>
                <!-- PLU Price Column -->
                <ng-container matColumnDef="pluPrice">
                    <mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
                    <mat-cell *matCellDef="let salesdata"> 
                        {{salesdata.pluPrice}} 
                    </mat-cell>
                </ng-container>
                <!---->
                <ng-container matColumnDef="total_amount">
                    <mat-header-cell *matHeaderCellDef> 
                        Total 
                    </mat-header-cell>
                    <mat-cell *matCellDef="let salesdata"> 
                        {{salesdata.pluPrice * salesdata.sold_count}} 
                    </mat-cell>
                </ng-container>
    <mat-header-row *matHeaderRowDef="displayedColumns2"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns2;"></mat-row>
            </mat-table>
        </div>
    </mat-expansion-panel>
</mat-accordion>

.TS:

//Get data from Sale1List
getSale1() {        
    this.customersService.getSale1()
    .subscribe(
        dataList => {                    
                this.updateDataTable(dataList);
         }               
     )       
 }


updateDataTable(dataList) { 
for(var i = 0;i < dataList.length; i++){
    var saleData = <SaleDataModel>dataList[i];

   var mySaleModelTest = this.mySaleModelArray2.find(x => x.name == dataList[i].name);
   if(mySaleModelTest == null){
       //first time creating the object with the bar name
       var tempArray = Array();
       tempArray.push(saleData);

       this.mySaleModelArray2.push(new Sale1Model(dataList[i].name,dataList[i].pluPrice * dataList[i].sold_count,tempArray));

   }else{                           
       //changing the object with the bar name because it already exist
       mySaleModelTest.total_sales = mySaleModelTest.total_sales + dataList[i].pluPrice * dataList[i].sold_count;
       mySaleModelTest.sales.push(saleData);
   }                    
   }    
}

3 answers

  • answered 2018-11-08 08:05 Amit Chigadani

    If you are simply pushing object or data into an array, angular wont detect it as changed property (which is bad). Because you did not completely re-assign to it.

    What you could do is :

    1. You can manually perform change detection
    2. Reassign entire array mySaleModelArray2 using temp array (not a good solution though).

  • answered 2018-11-08 08:09 trichetriche

    Use a custom trackby function with a unique return statement (for instance, IDs are supposed to be unique, or you can track on the property you change)

    *ngFor="let data of mySaleModelArray2; trackBy: customTB"
    
    customTB(item, index) {
      return `${item.id}-${index}`;
    }
    

  • answered 2018-11-08 08:24 Jacopo Sciampi

    ChangeDetectorRef will do the trick.

    Inject him in the constructor.

    constructor(
      ... 
      private cdr: ChangeDetectorRef,
      ...
    ) { }
    

    edit getSale1 like this in order to use the cdr:

    getSale1() {        
        this.customersService.getSale1()
        .subscribe(
            dataList => {                    
                    this.updateDataTable(dataList);
                    this.cdr.detectChanges();
             }               
         )       
     }
    

    But why I have to use the ChangeDetectorRef?

    Angular, by default, use the ChangeDetectionStrategy.default that use its logic to "wake-up" the component for the render. More spec here: https://angular.io/api/core/ChangeDetectionStrategy

    There are certain cases where this isn't enough. One case could be a very big nested *ngFor. Another case could be when the scope changes. Let's take as example this code:

    function something(){
      this; //A
      this.service.getData
      .subscribe( res => {
        this; //B
      });
    }
    

    this releated to the comment A is meant to be the component stuff. If you put a breakpoint in that point, you will see the component methods and properties.

    this releated to the .subscribe (comment B) is not the component anymore, but a subscription. That's the scope change, and it can cause some refresh problems.

    In javascript this happens more often, IE: using this inside a callback, the scope is different. But angular is clever, and "knows" that even in the subscription, if you do this.componentMethod() you are trying to call a function that belongs to the component.

    So why use the cdr?

    As I said, there are some cases when Angular does not wake up its renderer. Since every situation is not the same, it's quite impossibile to define an absolute answer to this. That's why my example of the scope changes. What cdr.detectChanges() does, is to allow the method to inform the Angular's rendered to force the render of its component.html. In this way, no matter which strategy are you using (even if it's .onPush) the component will be re-rendered.

    But be careful. you have to think what you are doing before implementing this. For example, re-render the html fire the ngOnChanges event. So you could enter an endless loop.

    More info about cdr: https://angular.io/api/core/ChangeDetectorRef

    Hope that this cleared out some doubts.