Angular array changes detection

11.1k views Asked by At

I am a beginner with Angular, and i can't find proper solution for my problem.

I have a component containing table filled with list of items (each item in list is in another component), and there is third component containing filter. Filter contains multiple checkboxes and a filter button.

I send boolean array of checkboxes states from filterComponent through itemsListComponent to singleItemComponent and it works fine. My problem is changes detection. When I used NgDoCheck with differs it works always when I click at checkbox and filter button instead only filter button.

I tried NgOnChanges but it worked only once and then it didn't see any changes of values into array.

This is my SingleItemsComponent (I think you don't need others to help me solve this). Look at this and show me any example of solving this problem please.

import { Component, OnInit, OnChanges,ViewChild, AfterViewInit, Inject,     Input, DoCheck, KeyValueDiffer, KeyValueDiffers, SimpleChanges, SimpleChange,     ChangeDetectionStrategy, ChangeDetectorRef, IterableDiffers } from     '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { animate, state, style, transition, trigger } from     '@angular/animations';
import { MatTableDataSource, MatPaginator, MatSort,     throwToolbarMixedModesError } from '@angular/material';
import { IPOLine } from 'src/app/models/po-line';
import { POLineService } from 'src/app/services/po-line.service';
@Component({
  selector: 'app-po-lines-list',
  templateUrl: './po-lines-list.component.html',
  styleUrls: ['./po-lines-list.component.css'],
  animations: [
    trigger('detailExpand', [
      state('collapsed', style({ height: '0px', minHeight: '0', display:     'none' })),
      state('expanded', style({ height: '*' })),
      transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4,     0.0, 0.2, 1)')),
    ]),
  ],
})
export class POLinesListComponent implements OnInit, DoCheck{
  isLogged = false;
  login: string;
  dataSource = new MatTableDataSource<IPOLine>();
  expandedElement: IPOLine;
  errorMessage: string;
  response: any;
  today = new Date(); // (1990, 0, 1);
  isLoadingList = true;

  differ: any;

  @Input() sentData: boolean[];
  _sentData = this.sentData;
  ngDoCheck(){

    var changes = this.differ.diff(this._sentData);
    if (changes){
      console.log('changes detected changes detected changes detected     changes detected changes detected ');
    }
    else
    console.log('changes not detected changes not detected changes not     detected changes not detected ');
  }


  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  constructor(
    //private cd: ChangeDetectorRef,
    private differs: KeyValueDiffers,
    private _POLineService: POLineService,
    private activatedRoute: ActivatedRoute) {
      this.differ = differs.find({}).create();
  }

  ngOnInit() {
    // Assign the data to the data source for the table to render
    this.login = this.activatedRoute.snapshot.paramMap.get('login');
    this._POLineService.getUserPOLines(this.login)
      .subscribe(data => {
        this.dataSource.data = data;
          },
        error => {
          this.errorMessage = <any>error;
          this.isLoadingList = false;
        }
        ,
        () => {
          // console.log('eee' + this.dataSource.data);
          this.isLoadingList = false;
          this.dataSource.data.forEach(x => x.isExpired = new     Date(x.PromisedDate) < new Date() ? true : false);
        }
      );

    this.dataSource.paginator = this.paginator;
    this.dataSource.sort = this.sort;
  }
}

I'm sorry for the mess in a code but I tried so many things i don't even remember what was what. Thanks for any help.

FilterComponent.ts:

import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';

@Component({
  selector: 'app-filters',
  templateUrl: './filters.component.html',
  styleUrls: ['./filters.component.css']
})
export class FiltersComponent implements OnInit {

  @Output() filterByClass = new EventEmitter();
  filterByTTR = false;
  filterByTBR = false;
  filters:boolean[] = [];
  constructor() { }

  ngOnInit() {
  }

log() {
  if(!this.filterByTTR)
   console.log('TTR not checked');
   else
   console.log('TTR checked');
  if (!this.filterByTBR)
   console.log('TBR not checked');
   else
   console.log('TBR checked');
   this.filters[0] = this.filterByTTR;
   this.filters[1] = this.filterByTBR;

   this.filterByClass.emit(this.filters);
}

Part of FilterComponent.html:

<mat-card-content class="col">
<mat-checkbox [checked]="filterByTTR" (change)="filterByTTR = !filterByTTR">TTR</mat-checkbox>
<mat-checkbox [checked]="filterByTBR" (change)="filterByTBR = !filterByTBR">TBR</mat-checkbox>
<!-- <mat-checkbox [(ngModel)]="filterByTBR"ref-TBR>TBR</mat-checkbox> -->
<div class="d-flex justify-content-between">
  <button mat-raised-button  color="primary">Clear</button>
  <button mat-raised-button (click)="log()" color="primary">Apply</button>
</div>

PoLinesCompoenent.ts:

import { Component, OnInit, NgModule, ViewChild, Input } from 
'@angular/core';

@Component({
  selector: 'app-po-lines',
  templateUrl: './po-lines.component.html',
  styleUrls: ['./po-lines.component.css']
})    
export class POLinesComponent implements OnInit {
  count: boolean[];
  constructor() { }    
  ngOnInit(){}      
  displayCounter(count) {
    console.log('first value is: ' + count[0] + ' second value is: ' +     count[1]);
    this.count = count;
  }

PoLinesComponent.html:

<div class="container">
  <div class="row">
    <div class="side-bar col">
      <app-filters (data)='displayCounter($event)'></app-filters>
    </div>
    <div class="content col">
      <app-po-lines-list [sentData]="count"></app-po-lines-list>
    </div>
  </div>
</div>
2

There are 2 answers

3
Nabil Shahid On BEST ANSWER

You can use the setter with input to detect the changes if 'sentData` input is changed by the parent. The setter function will be called everytime the input changes from outside. For example you can do something like:

  @Input('sentData') 
  set sentData(value:any){
     //this function will be called whenever value of input changes. the value parameter will contain the new changed 
      this._sentData=value;
      //things to perform on change go here
  }
  _sentData;

See this link for more details about input change detection : Intercept @Input property change in Angular Hope this helps.

If the value passed in input via parent is not a primitive type then angular wont detect the changes in that object and the setter wont be called in the child i.e. POLinesListComponent. For fixing this issue and getting your change detected, you can use Object.assign function of javascript which will change the reference of this.count thus triggering angular change detection. You can do something like the following in your displayCounter function:

displayCounter(count) {
    console.log('first value is: ' + count[0] + ' second value is: ' +     count[1]);
    //if count is changed, then only trigger change.
    if(JSON.stringify(this.count)!=JSON.stringify(count))
    {

      this.count= []; 
      Object.assign(this.count, count);
    }

  }

I have even created a stackblitz similar to your implementation in which the change is being detected properly and the value of new count is being logged to the console everytime filter is applied. Here Input Change Detection for Object

2
pascalpuetz On

Angular change detection only checks if something changed by reference. In your parent component, you have to create a new array and assign it to the variable everytime a change occurs to your entries inside your array. Just think of arrays as Immutables.