4
votes

I have a mat-table that loads data from firebase.

all-matches.component.html

...
<mat-table #table [dataSource]="dataSource" class="mat-elevation-z8">
    ...
    <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>
    ...
    <mat-header-row *matHeaderRowDef="columnsToDisplay"></mat-header-row>
    <mat-row *matRowDef="let row; columns: columnsToDisplay;"></mat-row>
</mat-table>
...

Per recommendations online (that I don't fully understand yet), I opted to use a dataSource object to populate my table. The dataSource gets instantiated in all-matches.component.ts:

all-matches.component.ts

...
@Component({
  selector: 'app-all-matches',
  templateUrl: './all-matches.component.html',
  styleUrls: ['./all-matches.component.scss']
})
export class AllMatchesComponent implements OnInit, OnDestroy, AfterViewInit {
  private columnsToDisplay = ['rank','weightClass', 'ageClass','athlete1Name', 'athlete2Name', 'gender','tournamentName','location', 'date', 'matchRating', 'videoUrl']; //TODO make this dynamic somehow
  private loading = true;
...
@ViewChild(MatPaginator) paginator: MatPaginator;

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

  ngOnInit() {
    ...
    this.pageSize = 2; //TODO increase me to something reasonable
    this.dataSource = new MatchDataSource(this.dbService);
    this.dataSource.loadMatches('test', '', '', 0, this.pageSize);
    this.dbService.getMatchCount().subscribe(results=>{
      this.matchCount = results;
    });
    ...
    }

MatchDataSource.model.ts

import {CollectionViewer, DataSource} from "@angular/cdk/collections";
import { BehaviorSubject ,  Observable , of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { Match } from './match.model';
import { DatabaseService } from './database.service';
import { Injectable } from '@angular/core';

@Injectable()
export class MatchDataSource implements DataSource<Match> {

  private matchesSubject = new BehaviorSubject<Match[]>([]);
  private loadingMatches = new BehaviorSubject<boolean>(false);
  public loading$ = this.loadingMatches.asObservable();

  constructor(private dbService: DatabaseService) {}

  connect(collectionViewer: CollectionViewer): Observable<Match[]> {
    return this.matchesSubject.asObservable();
  }

  disconnect(collectionViewer: CollectionViewer): void {
    this.matchesSubject.complete();
    this.loadingMatches.complete();
  }

  loadMatches(matchId: string, filter = '',
  sortDirection='asc', pageIndex: number, pageSize: number) {
    this.loadingMatches.next(true);
    this.dbService.getKeyOfMatchToStartWith(pageIndex, pageSize).subscribe(keyIndex=>{
      this.dbService.getMatchesFilteredPaginator(keyIndex, pageSize).pipe(
        catchError(()=> of([])),
        finalize(()=>{
          //TODO the tutorial here https://blog.angular-university.io/angular-material-data-table/ toggled the loading spinner off here, but it seemed to work better below for me?
        })
      )
      .subscribe(matches => {
        let results = this.makeIntoArray(matches);
        this.matchesSubject.next(results);
        // console.log("loading done");
        this.loadingMatches.next(false);
      });
    });
  }

  makeIntoArray(matches: any){
    let results = []; //TODO there should be a way to tighten the below up
    for(var i in matches){
      let obj1 = {id:matches[i].id};
      if(matches[i].matchDeets){
        let obj2 = matches[i].matchDeets;
        obj1 = Object.assign({}, obj1, obj2);
      }
      results.push(obj1);
    }
    // console.log(results);
    return results;
  }
}

The rows load just fine (although I have some concern with scaling, since I'm counting total rows with an observable (why is there no straightforward way to count entries in a firebase node??)).

However, when I reload the page, the spinner is never dismissed and the rows never populate. I welcome any advice!

Reproducing my issue:

git clone https://github.com/Atticus29/dataJitsu.git
cd dataJitsu
git checkout matTableSO

Make an api-keys.ts file in /src/app and populate it with the text to follow

api-keys.ts

export var masterFirebaseConfig = {
    apiKey: "AIzaSyCaYbzcG2lcWg9InMZdb10pL_3d1LBqE1A",
    authDomain: "dataJitsu.firebaseapp.com",
    databaseURL: "https://datajitsu.firebaseio.com",
    storageBucket: "",
    messagingSenderId: "495992924984"
  };

export var masterStripeConfig = {
  publicApiTestKey: "pk_test_NKyjLSwnMosdX0mIgQaRRHbS",
  secretApiTestKey: "sk_test_6YWZDNhzfMq3UWZwdvcaOwSa",
  publicApiKey: "",
  secretApiKey: ""
};

Then, back in your terminal session, type:

npm install
ng serve
1
Can you provide a version with just TableComponent on StackBlitz? Instead of creating an account can you provide a test username/password for login. I also noticed you are loading 6 scripts in all-matches.component.html. May I know the reasoning behind this as well. - Sid
I won't be able to provide a StackBlitz version on the timeline of the bounty, unfortunately (there are a lot of moving parts just to get the mat-table and its associated classes set up). As for the scripts, they are a vestige of something I was doing in the past and should no longer be necessary for the current problem. - Atticus29
As for username/password, use [email protected] for username and "ValidPassword23" for the password. - Atticus29

1 Answers

1
votes

Creating BehaviorSubject object & then converting it to Observable is good way but the changes are occurring in different context than the AllMatchesComponent. So, not only you've to subscribe to changes in $loading in component class but also updating model value (change detection) You can do that in below ways:

1. Using NgZone.run() : NgZone is an injectable service for executing work inside or outside of the Angular zone. Running functions via run will allow you to reenter Angular zone from a task that was executed out of context. So, inject NgZone in both component & MatchDataSource:

import { NgZone } from '@angular/core';
constructor(..., private nz: NgZone) { }

Then, in AllMatchesComponent update datasource object creation:

this.dataSource = new MatchDataSource(this.dbService, this.nz);

And for this update service code as:

  loadMatches(matchId: string, filter = '',
  sortDirection='asc', pageIndex: number, pageSize: number) {
    this.loadingMatches.next(true);
    this.dbService.getKeyOfMatchToStartWith(pageIndex, pageSize).subscribe(keyIndex=>{
      this.dbService.getMatchesFilteredPaginator(keyIndex, pageSize).pipe(
        catchError(()=> of([])),
        finalize(()=>{
          //TODO 
        })
      )
      .subscribe(matches => {
        let results = this.makeIntoArray(matches);
        this.nz.run(() => {
          this.matchesSubject.next(results);
          this.loadingMatches.next(false);
        });

      });
    });
  }

Here only thing I've updated in your code is, in subscribe I've enclosed .next() call with NgZone.run(). No other change required & async pipe should work as expected

You can refer Github Repo example. Check for AllMatchesComponent & MatchDataSource

2. In another approach you can just skip the use of async pipe & just subscribe to datasource.$loading and then update the change in model var using ChangeDetectorRef.

import { ChangeDetectorRef } from '@angular/core';
constructor(..., private cdr: ChangeDetectorRef ) {  }

ngOnInit() {

//Keep other code as it is
// Uncomment loading$ subscribe & update it as below

  this.dataSource.loading$.subscribe(result =>{
     this.showLoader = result;
     this.cdr.detectChanges();
  });
}

Make no changes in MatchDataSource service. Update loader template code to:

<div class="spinner-container" *ngIf="showLoader">
        <mat-spinner id="spinner"></mat-spinner>
</div>

This will then work as expected & changes will updated.