12

I'm running two MatTables in different components with data sources from different observables. One of my tables sort functionality is working fine and but on my second table it seems as if the @ViewChild for MatSort doesn't initialize during ngOnInit.

Data renders and the material table has sort buttons but the functionality is nothing. Checked my imports and the module and everything is fine.
On logging the MatSort one component logs a MatSort object and the other is undefined

Sorting not working.

Feed.component:

   import { PostService } from './../../services/post.service';
   import { Post } from './../../models/post';
   import { Component, OnInit, ViewChild, ChangeDetectorRef} from 
     '@angular/core';
   import { MatSort, MatTableDataSource, MatCheckbox, MatPaginator, 
     MatTabChangeEvent, MatDialog, MatDialogActions, MatTable}  from 
   "@angular/material"



export class FeedComponent implements OnInit {
  @ViewChild(MatSort) sort: MatSort;
  @ViewChild(MatPaginator) paginator: MatPaginator;
  postData: Post[] =[];
  dataSource : MatTableDataSource<any> 
  currentUser = JSON.parse(localStorage.getItem('user'))
  displayedColumns:string[] = ['User','Title', "Description", 
  "Contact" ]
  posts = this.ps.getPosts();

  constructor(private ps: PostService, public dialog:MatDialog, 
    public change:ChangeDetectorRef, public ms:MessageService) { 

  }



refreshPosts(){
   console.log(this.sort) < -------comes back undefined
  this.posts.subscribe(posts=>{
    this.dataSource.sort = this.sort
     this.postData = posts.filter(post => post.uid != 
       `${this.currentUser.uid}` && post.claimedBy 
        !=`${this.currentUser.uid}`);
     this.dataSource= new MatTableDataSource(this.postData)
     this.dataSource.paginator = this.paginator;
    });

  }
ngOnInit() {
   this.refreshPosts()
   console.log(this.sort)
   }


Post.service
  getPosts(){
    return  this.afs.collection('posts').snapshotChanges()
     .pipe(map(actions => 
     actions.map(this.documentToDomainObject)))
  }
 documentToDomainObject = _ => {
  const object = _.payload.doc.data();
  object.id = _.payload.doc.id;
  return object;
}

Now my next component initializes in the same way but the @ViewChild shows up as a MatSort Object

Message.component:

export class MessageComponent implements OnInit {


 @ViewChild(MatSort) sort: MatSort;
  userReceived: MatTableDataSource<any>;
  userSent: MatTableDataSource<any>;
  displayedColumns:string[] = ["createdAt",'author',"title", "Delete"]
  sentColumns:string[] = ["createdAt","recipient", "title", "Delete"]


  currentUserId= this.currentUser['uid']
  currentUsername = this.currentUser['displayName']
  recipient:any;
  selectedMessage: MatTableDataSource<Message>;
  messageColumns= ['From','Title',"Body"];




  constructor(public ms:MessageService, public change:ChangeDetectorRef, public dialog: MatDialog  ) { }

  ngOnInit() {
    console.log(this.sort)
    this.updateMessages()
    this.currentUserId = this.currentUserId;
    this.currentUsername = this.currentUsername;

 }


updateMessages(){
    this.ms.getUserSent().subscribe(messages => {
      console.log(this.sort) <------logs MatSort object
      this.userSent = new MatTableDataSource(messages)
      this.userSent.sort = this.sort
      console.log(this.userSent.sort)
      console.log(this.userSent.data)

    })

message.service

 getUserSent() {
    let messages:any[] = [];
    this.userSent = this.afs
      .collection('messages', ref => ref.where('uid', '==', `${this.currentUser.uid}`)).snapshotChanges() 
return this.userSent
  }

feed.component.html

<div class = "mat-elevation-z8">
    <mat-form-field>
        <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search Posts">
      </mat-form-field>
  <table matSort mat-table [dataSource]="dataSource" style="text-align:left">
      <ng-container matColumnDef="User">
          <th mat-header-cell *matHeaderCellDef mat-sort-header>User</th>
          <td mat-cell *matCellDef="let post">{{post.displayName}}</td>
       </ng-container>

  <ng-container matColumnDef="Title">
    <th mat-header-cell *matHeaderCellDef>Title</th>
    <td mat-cell *matCellDef="let post">{{post.title | truncate:15:false }}</td>
 </ng-container>

  <ng-container matColumnDef="Description">
    <th mat-header-cell *matHeaderCellDef >Description</th>
    <td mat-cell *matCellDef="let post">{{post.description | truncate: 20 : false}}</td>
  </ng-container>





  <ng-container matColumnDef="Contact">
    <th mat-header-cell *matHeaderCellDef> Contact </th>
    <td mat-cell *matCellDef="let post">
      <button  id="{{post.id}}" color="primary" (click)="openDialog($event.target.id)" style = "outline:none" value={{post.id}}>Claim</button>
    </td>

  </ng-container>

  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef='let row; columns: displayedColumns'></tr>
</table>
</div>
  <mat-paginator [length]="this.postData.length" [pageSize]="5" [pageSizeOptions]="[5,10,25]"></mat-paginator>

I really cant find why in my first component the sort returns undefined when in my second, working component it returns and object. Am I missing something about the order of @ViewChild?

Timotronadon
  • 315
  • 1
  • 2
  • 15

2 Answers2

19

From official docs: https://angular.io/api/core/ViewChild#description

View queries are set before the ngAfterViewInit callback is called.

In order to get @ViewChild property inited, you need to call it in ngAfterViewInit lifecycle hook.

export class MessageComponent implements OnInit, AfterViewInit {

   @ViewChild(MatSort) sort: MatSort;

   ngAfterViewInit(){
      console.log(this.sort)
   }
}

If you are using Angular 8, you need to refactor the implementation of @ViewChild properties since static flag is required

Harun Yilmaz
  • 8,281
  • 3
  • 24
  • 35
  • Thanks Harun, but my second component has no ngAfterViewInit either - if this was the case shouldn't we expect that not initialize even though it is? – Timotronadon Jul 09 '19 at 18:12
  • 1
    `ngAfterViewInit` makes sure that the property is initialized (if there is a view child in template). You can however get the property inited -for example- after a button click or a service call because it **might** be inited in the mean time (but not guaranteed). And can you please share the template where you placed `MatSort` that you want to access? – Harun Yilmaz Jul 09 '19 at 18:16
  • That should do it. Is the view rendering before the observable is returned? Still not sure why this is happening in one component but not the next. – Timotronadon Jul 09 '19 at 18:46
  • Can you please help me? I'm getting this error: `ERROR Error: "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'model: undefined'.` – Tanzeel Feb 15 '20 at 09:05
  • Hello @Tanzeel I would like to help but I'm really busy these days. You can have a look at this SO thread: https://stackoverflow.com/questions/43375532/expressionchangedafterithasbeencheckederror-explained – Harun Yilmaz Feb 15 '20 at 16:47
1

the problem in FeedComponent is that you are making this.dataSource.sort = this.sort assignment before initializing this.dataSource

refreshPosts(){
  console.log(this.sort) < -------comes back undefined
  this.posts.subscribe(posts=>{
     this.postData = posts.filter(post => post.uid != `${this.currentUser.uid}` && post.claimedBy !=`${this.currentUser.uid}`);
     this.dataSource= new MatTableDataSource(this.postData)
     this.dataSource.sort = this.sort // make assignment after initializing this.dataSource
     this.dataSource.paginator = this.paginator;
    });
  }

please note that console.log(this.sort) will still print undefined because of lifecycle sequences. on ngOnInit view queries are not set.

so the question arises; then how would this.dataSource.sort = this.sort assignment work in ngOnInit in MessageComponent ?

the answer is long but to put it in simple terms; because that code gets executed in subscribe callback. since the code in subscribe callback gets executed when observable emits, an asynchronous operation happens. and that async operation happens in a subsequent change detection cycle after a cycle where ngAfterViewInit hook gets executed.

you are not getting undefined output in your second component because that console.log statement is also executed in a subscribe callback. moving that log statement out of subscribe callback will also print undefined.

if you place console.log statements in ngAfterViewInit hook, they both will print actual values whether they are placed in a subscribe callback or not.

as a summary;

make assignment after initializing this.datasource

this.dataSource= new MatTableDataSource(this.postData)
this.dataSource.sort = this.sort // make assignment after initializing 

and do it in ngAfterViewInit hook, even though it works in ngOnInit due to the async operation.

ysf
  • 4,634
  • 3
  • 27
  • 29
  • But why I'm getting this error: `"ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.` – Tanzeel Feb 15 '20 at 09:11
  • ExpressionChangedAfterItHasBeenCheckedError may have several causes and it is hard to guess why you are getting it. if you can post your problem and code as a question you might get help. and [here is a good article](https://indepth.dev/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error/) to get some grasp of ExpressionChangedAfterItHasBeenCheckedError – ysf Feb 21 '20 at 06:54