Home > front end >  Functions as Element Attributes and the Angular ExpressionChangedAfterItHasBeenCheckedError
Functions as Element Attributes and the Angular ExpressionChangedAfterItHasBeenCheckedError

Time:01-11

I'm creating a summary row on a tree table that also has filtering (I'm using the ng Prime component library, but I don't think that's germane to the error or question). I want my summary row to only summarize the information currently in view, thus the summary should change when the tree is expanded or when a filter is applied.

I achieved this goal by adding row data to a map via a function I set as an element attribute [attr.summaryFunction]="summaryFunction(i,rowData[col.field])", and reinitializing the map with ngDoCheck. Without this step, reinitializing the map in ngDoCheck, totals accumulate with every repaint or change and result in incorrect values. (I tried reinitializing the map with several other lifecycle hooks ngOnInit, ngOnViewInit, and ngOnChanges, but the only lifecycle hook that effectively reinitialized the map was ngDoCheck).

My approach works perfectly (yay me!), but I get an ExpressionChangedAfterItHasBeenCheckedError in the browser, three times, for each column. To address this error, I tried all 4 approaches to fixing the error recommended in the documentation:

  1. using different lifecycle hooks --> totals are incorrect
  2. I added this.cd.detectChanges() to summary function --> infinite loop
  3. I wrapped the if statement in setTimeout(() => if(statement){}) --> infinite loop
  4. Promise.resolve().then(() => if(statement){}) --> infinite loop

Again, my approach currently works exactly as I want it to, I just want these annoying errors to go away. I'd also like to understand why the usual fixes cause an infinite loop.

Here's my TS layer;

export class PrimetreeComponent implements OnInit, DoCheck {
sampleData: TreeNode[] = []
columns: any[] = [];
summaryMap = new Map();
columnIndex = 0;

constructor(private nodeService: NodeService, private cd: ChangeDetectorRef) {}


  ngOnInit(): void {
    this.nodeService.getFilesystem().subscribe(data => {this.sampleData = data});

      this.columns = [
      {field: 'name', header: 'name'},
      {field: 'size', header: 'size'},
      {field: 'type', header: 'type'}]
  }

 ngDoCheck(): void {
  this.summaryMap = new Map();
 }

  summaryFunction(columnIndex:number, rowData:any){
    
    if(this.summaryMap.has(columnIndex)){
      //if row data is a non-number string we just count the rows
      if(Number(rowData)){
        this.summaryMap.set(columnIndex, this.summaryMap.get(columnIndex) Number(rowData));
      } else {
        this.summaryMap.set(columnIndex, this.summaryMap.get(columnIndex) 1)
      }
    } else {
      if(Number(rowData)){
        this.summaryMap.set(columnIndex, Number(rowData));
      } else {
        this.summaryMap.set(columnIndex, 1)
      }
    } 
  }

}

And here's my HTML Template:

<p-treeTable #tt [value]="sampleData" [columns]="columns">
  <ng-template pTemplate="caption">
    <div style="text-align: right">
      <span >
        <i ></i>
        <input
          pInputText
          type="text"
          size="50"
          placeholder="Global Filter"
          (input)="tt.filterGlobal($any($event.target).value, 'contains')"
          style="width:auto"
        />
      </span>
    </div>
  </ng-template>
  <ng-template pTemplate="header" let-columns>
    <tr>
      <th *ngFor="let col of columns">
        {{ col.header }}
      </th>
    </tr>
    <tr>
      <th *ngFor="let col of columns">
        <input
          pInputText
          type="text"
          
          (input)="
            tt.filter($any($event.target).value, col.field, col.filterMatchMode)
          "
        />
      </th>
    </tr>
  </ng-template>
  <ng-template pTemplate="body" let-rowNode let-rowData="rowData" >
    <tr>
      <td *ngFor="let col of columns; let i = index;" [attr.summaryFunction]="summaryFunction(i,rowData[col.field])">
        <p-treeTableToggler
          [rowNode]="rowNode"
          *ngIf="i == 0"
          (columnIndex)="i"
        ></p-treeTableToggler>
        {{ rowData[col.field] }}
      </td>
    </tr>
  </ng-template>
  <ng-template pTemplate="emptymessage">
    <tr>
      <td [attr.colspan]="columns.length">No data found.</td>
    </tr>
  </ng-template>
<ng-template pTemplate="summary">
  <tr>
    <td *ngFor="let col of columns;  let i = index;">
      {{ col.header }}--sumMap--{{summaryMap.get(i) }}
    </td>
  </tr>
</ng-template>

CodePudding user response:

I am not familiar with the approach of binding a function to an elements attribute, but it seems kind of hacky to me. Correct me if I'm wrong, but the reason for this is to have a way to trigger the function when the node is added to the dom?

I did not test this myself, but I think you could make use of the @ContentChildren decorator see docs. It exposes an observable that you can subscribe to to react to changes of the content dom. Ideally, you could run the summary creation and updating the summaryMap in one step.

Update:

I played around with the idea a bit. One solution for what you are trying to achieve is to use @ViewChildren decorator to get a QueryList of all the rows currently rendered. This can be achieved in the following way:

 <ng-template pTemplate="body" let-rowNode let-rowData="rowData" >
    <tr [ttRow]="rowNode" #tableRow [attr.file-size]="rowData.size">
      <td>
        <p-treeTableToggler [rowNode]="rowNode"></p-treeTableToggler>
        {{ rowData.name }}
      </td>
      <td>{{ rowData.size }}</td>
      <td>{{ rowData.type }}</td>
    </tr>
  </ng-template>

Note the template ref on <tr [ttRow]="rowNode" #tableRow [attr.file-size]="rowData.size"> along with binding the rowData.size to an attribute of the element.

You can then grab that list of rows rows that are currently mounted to the dom like so:

// inside your component

@ViewChildren('tableRow') view_children!: QueryList<ElementRef>;

// get the list, calculate initial sum and subscribe to changes. 
// We do that in ngAfterViewInit lifecycle hook as the 
// list is guaranteed to be available at that point of time:

ngAfterViewInit() {
  this.calculateSum(this.view_children);

  this.view_children.changes.subscribe(
    (list: QueryList<ElementRef>) =>
      this.calculateSum(list)
  );
}


You can find a running example of this here.

  • Related