Cannot read property 'page' of undefined when making a material paginator

Atticus29 picture Atticus29 · Jun 21, 2018 · Viewed 11.9k times · Source

I have been following the Angular Material Data Table paginator tutorial here.

When they construct their paginator, they declare a paginator using

@ViewChild(MatPaginator) paginator: MatPaginator;

in their component.ts file. With this declaration, they are able to access properties later in the same file:

 ngAfterViewInit() {
        this.paginator.page
            .pipe(
                tap(() => this.loadLessonsPage())
            )
            .subscribe();
    }

without issue.

When I do the same thing in my component:

export class AllMatchesComponent implements OnInit, OnDestroy, AfterViewInit {
...
  @ViewChild(MatPaginator) paginator: MatPaginator;
...
ngAfterViewInit(){
      console.log(this.paginator);
      this.paginator.page
        .pipe(
          tap(()=> this.loadMatchesPage())
        )
        .subscribe();
    }
...

, I get the following error in the console:

ERROR TypeError: Cannot read property 'page' of undefined at AllMatchesComponent.ngAfterViewInit (all-matches.component.ts:50)

When I try to take a look at their example repo (going to branch 2-data-table-finished), the code includes all of the filtering and sorting syntax, which I haven't gotten to in the tutorial yet. In other words, their example code does not resemble the step I'm on in the tutorial, and there are no other branches that capture the code in the simpler state.

I'm at a loss for figuring out why paginator is undefined. Any ideas?

My personal repo can be found here:

git clone https://github.com/Atticus29/dataJitsu.git
cd dataJitsu
git checkout SO-paginator-freeze

For convenience, here's the entire all-matches.component.ts:

import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
import { D3Service } from '../d3.service';
import { DatabaseService } from '../database.service';
import { TextTransformationService } from '../text-transformation.service';
import * as firebase from 'firebase/app';
import { MatTableDataSource, MatSort } from '@angular/material';
import { MatPaginatorModule } from '@angular/material/paginator';
import { DataSource } from '@angular/cdk/table';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import { MatchDataSource } from '../matchDataSource.model';
import { AuthorizationService } from '../authorization.service';
import { Subject } from 'rxjs/Subject';
import { tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material';

@Component({
  selector: 'app-all-matches',
  templateUrl: './all-matches.component.html',
  styleUrls: ['./all-matches.component.scss']
})
export class AllMatchesComponent implements OnInit, OnDestroy, AfterViewInit {
  private dataSource: MatchDataSource;
  private columnsToDisplay = ['rank','weightClass', 'ageClass','athlete1Name', 'athlete2Name', 'gender','tournamentName','location', 'date', 'matchRating', 'videoUrl']; //TODO make this dynamic somehow
  private loading = true;
  user: any = null;
  private ngUnsubscribe: Subject<void> = new Subject<void>();
  private matchCount: number;
  private pageSize: number;

  @ViewChild(MatPaginator) paginator: MatPaginator;

  constructor(private authService: AuthorizationService, private d3Service: D3Service, private dbService: DatabaseService, private textTransformationService: TextTransformationService) { }

  ngOnInit() {
    this.authService.getCurrentUser().takeUntil(this.ngUnsubscribe).subscribe(user=>{
      this.user = user;
    },err=>{
      console.log(err);
    });
    this.pageSize = 2;
    this.dataSource = new MatchDataSource(this.dbService);
    this.dataSource.loadMatches('test', '', '', 0, this.pageSize);
    this.dbService.getMatchCount().subscribe(results=>{
      this.matchCount = results;
    });
    }

    ngAfterViewInit(){
      console.log(this.paginator);
      this.paginator.page
        .pipe(
          tap(()=> this.loadMatchesPage())
        )
        .subscribe();
    }

    loadMatchesPage(){
      this.dataSource.loadMatches('TODO', '', 'asc', this.paginator.pageIndex, this.paginator.pageSize);
    }

    ngOnDestroy(){
      console.log("onDestroy is called");
      this.ngUnsubscribe.next();
      this.ngUnsubscribe.complete();
    }
}

And here's the corresponding html:

<script type='text/javascript' src='http://d3js.org/d3.v3.min.js'></script>
<script type='text/javascript' src='https://cdn.firebase.com/v0/firebase.js'></script>
<script type='text/javascript' src='d3fire.min.js'></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js?2.1.3"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.geom.js?2.1.3"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.layout.js?2.1.3"></script>
<br>
<div class="" *ngIf='user'>
  <div class="spinner-container" *ngIf="loading$">
        <mat-spinner id="spinner"></mat-spinner>
  </div>
  <mat-table #table [dataSource]="dataSource" class="mat-elevation-z8" *ngIf="!loading$">
    <!-- TODO add paidStatus to ngIf -->
    <ng-container matColumnDef="rank">
      <mat-header-cell *matHeaderCellDef> Rank </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.rank}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="weightClass">
      <mat-header-cell *matHeaderCellDef> Weight Class </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.weightClass}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="ageClass">
      <mat-header-cell *matHeaderCellDef> Age Class </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.ageClass}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="athlete1Name">
      <mat-header-cell *matHeaderCellDef> Athlete 1 </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.athlete1Name}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="athlete2Name">
      <mat-header-cell *matHeaderCellDef> Athlete 2 </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.athlete2Name}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="gender">
      <mat-header-cell *matHeaderCellDef> Gender </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.gender}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="location">
      <mat-header-cell *matHeaderCellDef> Location </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.location}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="tournamentName">
      <mat-header-cell *matHeaderCellDef> Tournament </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.tournamentName}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="date">
      <mat-header-cell *matHeaderCellDef> Date </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.date | date:'shortDate'}} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="videoUrl">
      <mat-header-cell *matHeaderCellDef> Video </mat-header-cell>
      <mat-cell *matCellDef="let entry"> <a href='{{entry.videoUrl}}'>Click</a> </mat-cell>
      <!-- <i class="material-icons right">video</i> -->
    </ng-container>
    <ng-container matColumnDef="matchRating">
      <mat-header-cell *matHeaderCellDef> Match Rating </mat-header-cell>
      <mat-cell *matCellDef="let entry"> {{entry.matchRating}}</mat-cell>
      <!-- <i class="material-icons right">video</i> -->
    </ng-container>

    <mat-header-row *matHeaderRowDef="columnsToDisplay"></mat-header-row>
    <mat-row *matRowDef="let row; columns: columnsToDisplay;"></mat-row>
  </mat-table>

  <mat-paginator [length]='matchCount' [pageSize]="2" [pageSizeOptions]="[2, 5, 10]"></mat-paginator>
</div>

Answer

Sergey Rudenko picture Sergey Rudenko · Jun 21, 2018

Based on your template you have the issue with attempting to grab ViewChild fir element that doesn’t exist in DOM due to *ngif directive in the parent div.

There are some options to solve this:

  1. You can replace *ngIf directive with [hidden] which hides your parent div but keeps it in the dom, thus your ViewChild reference will work

  2. More complex if you have to stick to *ngIf - is to ensure your child element can be grabbed as ElementRef (assign id to it and get reference to it AFTER it is created by ngIf set to true) and use of setter for your ViewChild as in this issue:

https://stackoverflow.com/a/41095677