4
votes

I have a data coming from an endpoint and put it into MatTableDataSource. I was able to get it working for MatSort and MatPaginator, but needed to use setTimeOut, which does not seems to be a proper way to do this. If I remove this, it will complain that 'Can not read property of sort undefined' which I assumed this is due to it's not initialized yet.

I also have tried:

  • to move moving it to afterviewinit, but the data was loaded after afterviewinit getting called so it still does not work
  • using this.changeDetectorRef.detectChanges() after this.datasource = new ... also does not work

This is my current code (which is working, but using settimeout)

<div *ngIf="!isLoading">
    <div *ngFor="let record of renderedData | async" matSort>

    // ... some html to show the 'record'

    <mat-paginator #paginator
        [pageSizeOptions]="[5, 10, 20]">
    </mat-paginator>
</div>

The Component

export class ResultsComponent implements OnInit, OnDestroy, AfterViewInit {
    dataSource: MatTableDataSource<any> = new MatTableDataSource();
    renderedData: Observable<any>;

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

    constructor(some service) {}

    ngOnInit() {
        const accountId = someOtherService.getAccountId();
        this.someService.getData(accountId)
            .subscribe((myData) => {
                    this.dataSource = new MatTableDataSource(myData);

                    // it won't work properly if it is not wrapped in timeout
                    setTimeout(() => {
                        this.dataSource.paginator = this.paginator;
                        this.sort.sort(<MatSortable>({id: 'created_on', start: 'desc'}));
                        this.dataSource.sort = this.sort;
                    });

                    this.renderedData = this.dataSource.connect();
                }
            });
    }

    ngAfterViewInit() {
    }

    ngOnDestroy(): void {
        if (this.dataSource) { this.dataSource.disconnect(); }
    }
}

The above code is working for me, I'm just looking the right way not to use settimeout if possible.

2
I had to do the same when creating the MatTableDataSource via subscription.. looking forward to seeing the answer on this.Marshal
A slightly better workaround may be to call ChangeDetectorRef.detecChanges() after this.dataSource = ..., instead of using setTimeout.ConnorsFan
@ConnorsFan tried it, it gave me "Cannot read property 'sort' of undefined at SafeSubscriber._next". I add in the constructor private changeDetectorRef: ChangeDetectorRef, and then below this.datasource = new Mat... this.changeDetectorRef.detectChanges();Harts
@ConnorsFan, detectChanges runs the full check of current view and all children, it's a very expensive way, and empty setTimeout doesn't look like evil comparing to this way IMHOCommercial Suicide

2 Answers

5
votes

There are a couple of lifecycle timing issues at play here, and when you think about it, this is right.

The MatSort is part of the view, so it isn't 'ready' during OnInit - it is undefined. So trying to use it throws the error.

The MatSort is ready in AfterViewInit, but things are further complicated by the fact that you need to 'apply' the sort to the datasource after performing the sort, and this triggers changes to the view by way of the renderedData that is 'connected' to the datasource. You therefore end up with an ExpressionChangedAfterItHasBeenCheckedError because the view initialization lifecycle hasn't completed but you are already trying to change it.

So you can't sort until the view is ready, and you can't apply the sort when you are notified that the view is ready. The only thing you can do is wait until the end of the component initialization lifecycle. And you can do that using setTimeout().

I don't think there is any way around both of these problems without setTimeout(), so in that case it doesn't matter whether you call it from OnInit or AfterViewInit.

A couple of other observations on your code:

You don't need to create a new instance of MatTableDataSource in your subscription. You can assign the result data to the already created datasource:

this.dataSource.data = myData;

This frees you up from having to connect the rendered data to the datasource afterward, so you can do it when you initialize the datasource:

dataSource: MatTableDataSource<any> = new MatTableDataSource();
renderedData: Observable<any> = this.dataSource.connect();
1
votes

There is actually a way to do this without using setTimeout. This works for Angular 9, I haven't tested it on previous versions so I'm not sure if it works there.

I found this solution here: https://github.com/angular/components/issues/10205

Instead of putting:

@ViewChild(MatSort) sort: MatSort;

use a setter for matSort. This setter will fire once matSort in your template changes (i.e. is defined the first time), it will not fire when you change your sorting by clicking on the arrows. This will look like this:

    @ViewChild(MatSort) set matSort(sort: MatSort) {
        this.dataSource.sort = sort;
    }

You can use the same solution for the paginator.