Search code examples
angulartypescriptangular-materialangular-datatablesangular-material-table

Component (AngularMatDataTable) doesn't re-render after data array changes in subscription in ANGULAR?


I'm creating a Parent/Child relationship in Angular 8. Parrent component invokes the service call which sets the observable roleList,then I pass this roleList to child component. In child component, I try to subscribe to the Input variable that child receives and instantiate MatDataTableSource with new values.

The problem is: Child component receives the value but the component doesn't re-render. After I invoke a change in the application (e.g: hovering mouse to another component that changes the state) it does show the data that is received from service.

These are the components:

Parent Component .ts:

import { Component, OnInit, ChangeDetectionStrategy } from 
'@angular/core';
import { RoleService } from '../role.service';
import { RoleTemplateList } from '../../../../core/models/role-template- 
list.model';
import { Observable } from 'rxjs';

@Component({
selector: 'kt-view-roles',
templateUrl: './view-roles.component.html',
styleUrls: ['./view-roles.component.scss'],
})
export class ViewRolesComponent implements OnInit {
roleList: Observable<RoleTemplateList[]>;
columns = ['id', 'name', 'skills', 'actions'];
actions = ['actions'];

constructor(private roleService: RoleService) {}

ngOnInit() {
    this.roleList = this.roleService.getRoles();
}
}

Parent Component .html:

<kt-portlet-header [class]="'kt-portlet__head--lg'">
    <ng-container ktPortletTitle>
        <h3 class="kt-portlet__head-title">
            <span>All Roles</span>
        </h3>
    </ng-container>
</kt-portlet-header>

<kt-portlet-body>
    <kt-data-table-new [columnsToDisplay]="columns" [data]="roleList"></kt-data-table-new>

</kt-portlet-body>

</kt-portlet>

This is the component that I try to subscribe and initialize MatTableDataSource with the result of subscription.

Child Component .ts:

import { Component, OnInit, Input, AfterViewInit, ViewChild, OnChanges, 
ChangeDetectionStrategy } from '@angular/core';
import { Observable, merge, of } from 'rxjs';
import { MatPaginator, MatSort, MatTableDataSource } from 
'@angular/material';
import { startWith, switchMap, map, catchError, filter, tap } from 
'rxjs/operators';

@Component({
selector: 'kt-data-table-new',
templateUrl: './data-table-new.component.html',
styleUrls: ['./data-table-new.component.scss']
})
export class DataTableNewComponent implements AfterViewInit {

@Input() columnsToDisplay: any[];
@Input() data: Observable<any[]>;
dataSource: MatTableDataSource<any>;
isLoadingResults = true;
dataLength = 0;

@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
@ViewChild(MatSort, {static: true}) sort: MatSort;

constructor() { }

ngAfterViewInit() {
    this.sort.sortChange.subscribe(() => this.paginator.pageIndex= 0);
    merge(this.sort.sortChange, this.paginator.page)
        .pipe(
            startWith({}),
            switchMap(() => {
                this.isLoadingResults = true;
                return this.data;
            }),
            map(data => {
                this.isLoadingResults = false;
                this.dataLength = data.length;
                return data;
            }),
            catchError(() => {
                this.isLoadingResults = false;
                return of([]);
            }),
        ).subscribe(value => this.dataSource = new MatTableDataSource(value));
}

}

Child Component .html:

<div class="example-container mat-elevation-z8">
<div class="example-loading-shade" *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="example-table-container">
    <table mat-table [dataSource]="dataSource" class="example-table" matSort matSortActive="created"
        matSortDisableClear matSortDirection="desc">
        <ng-container [matColumnDef]="column" *ngFor="let column of columnsToDisplay">
            <th mat-header-cell *matHeaderCellDef>
                {{column}}
            </th>
            <td mat-cell *matCellDef="let element"> {{element[column]}} </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
        <tr mat-row *matRowDef="let row; columns: columnsToDisplay;"></tr>
    </table>
</div>
<mat-paginator [length]="dataLength" [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons></mat-paginator>

Edit:

I have ChangeDetectionStrategy.OnPush in app.component.ts. When I modify it to default it works as expected. But why it doesn't work with onPush?


Solution

  • This is because you are creating an observable by merging this.sort.sortChange and this.paginator.page, and then subscribing to that observable and updating the mat table data. That means, whenever there's a change in sort or pagination, observable will emit a value and table will be updated in subscription. But, any values emited in your data observable is not handled.

    Solution:

    1) Merge this.data while creating the observable, this will allow subscription to read the values emitted by change in data.

    merge(this.data, this.sort.sortChange, this.paginator.page)
    

    2) (Preferred) Use async pipe while sending the data

    <kt-data-table-new [columnsToDisplay]="columns" [data]="roleList | async"></kt-data-table-new>
    

    Child component:

    @Input() data: any[];
    

    Async pipe automatically unsubscribe to the observable when the component is destroyed to avoid potential memory leaks. Async pipe also marks the component for check, which is really helpful if you use ChangeDetectionStrategy.OnPush.

    You can read more about it here.

    Edit:

    Having OnPush on AppComponent makes all the child component's changeDetectionStrategy onPush as well. And, in that case, change detector only runs for child component when any of the input value changes, which it doesn't(Observables emiting new values does not change the object reference). To manually mark the component to get checked by the changeDetector, inject"ChangeDetectorRef", and call markForCheck where you subscribe to the observable.

    constructor(private cd: ChangeDetectorRef) { }
    //in subscribe block after updating table
    this.cd.markForCheck();
    

    OR better approach is to use the AsyncPipe, as it does that for you.

    Also, you shouldn't have OnPush on AppComponent, only put that in your smart components, that will keep you more aware of dealing with change detection.