Angular Material 2 Data Table Connect to AngularFire2 or Firebase Service?

Viewed 6967

I wish it was just plug and play :-) I've been thrashing around with this for hours with none of my little experiments working. The md data table is new, so there is almost no divine knowledge on the Web yet. Finding a good way to connect Firebase to the table would be a good start. Any ideas?

Currently I have this setup. My code works great without the table with the standard Angular setup and code, using ngFor and creating a list from a template. So the code delivers the data from Firebase with AngularFire 2. Trying out the new md data table is the problem.

First, the template won't render. I know I have NgModule setup correctly so my suspicion is that the data source isn't connecting and creating this error. This is the error in the Chrome console.

Template parse errors:
Can't bind to 'dataSource' since it isn't a known property of 'md-table'.
1. If 'md-table' is an Angular component and it has 'dataSource' input, then verify that it is part of this module.

My members-search.component.html looks identical to the official docs except I changed the template binding:

<md-table #table [dataSource]="dataSource">

<ng-container cdkColumnDef="memberName">
    <md-header-cell *cdkHeaderCellDef> Name </md-header-cell>
    <md-cell *cdkCellDef="let row"> {{member.firstName}} {{ member?.lastName }} </md-cell>
</ng-container>

members-search.component.ts has these relevant parts:

import { DataSource } from '@angular/cdk';

@Injectable()
export class MembersAdminService {

  private members$: FirebaseListObservable<Member[]>;
  private dataSource: DataSource<any>;

  constructor(
      private af: AngularFireDatabase,
      @Inject(FirebaseApp) fb) {
        this.members$ = af.list('Members');
  }

And I dropped these data table functions into my working code in members-search.service.ts and used the same code in the connect() that I've been using elsewhere on this service.

// md table dataSource functions.
  public connect(): FirebaseListObservable<any> {
    return this.af.list('Members', {
        query: {
            orderByChild: 'lastName'
        }
    });
    // return this._exampleDatabase.dataChange;
  }

  public disconnect() {}

The data table docs and plunker create a data source and database in the component but it seems that most of that isn't necessary if I already have Firebase. I'm learning all this so I'm far from an expert at anything and maybe I'm missing something.

If you haven't got into this new setup before then here are the docs. The md table is built on top of the cdk table to give the cdk table the styling.

https://material.angular.io/components/table/overview

https://material.angular.io/guide/cdk-table

3 Answers

Thought I'd add my approach for anyone looking for this solution.

I've tried to make this reusable across collections. It supports retrieving data and sorting up and down by specified fields. Don't forget to add the sort fields to .indexOn in the firebase rules.

I haven't managed to get paging working because it's just too hard to work out the startKey!

firebase-datasource.ts

Started by defining a template datasource that I can reuse for all collections that require this.

import { Component } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { AngularFireDatabase } from 'angularfire2/database';

/**
 * Sortable Interface - Used for specifying the sort order.
 */
export interface Sort {
  field: string;
  direction: '' | 'asc' | 'desc';
}

/**
 * FirebaseDataSource is a templated datasource for Firebase. At this stage it
 * allows:
 *   *  Tracking data updates to the underlying datarecords.
 *   *  Sorting ascending and descending
 *
 * We have not implemented paging controls as its too difficult with NoSQL. It also
 * does not support multi-field sorting.
 */
export class FirebaseDataSource<T> extends DataSource<T> {
  /**
   * The datachange subscriber emits new data when the Firebase records are updated.
   */
  dataChange: BehaviorSubject<T[]> = new BehaviorSubject<T[]>([]);

  /**
   * The sort change is updated when the sort order is changed.
   */
  sortChange: BehaviorSubject<Sort> = new BehaviorSubject<Sort>({field: '', direction: ''});

  /**
   * Path tracks the path of the list of records. e.g. /items
   */
  path: string;

  /**
   * Keep for cleaning up the subscription
   */
  private _sub: Subscription;

  /**
   * Getters and setters for setting sort order.
   */
  get sort(): Sort {
    return this.sortChange.value;
  }

  set sort(sort: Sort) {
    this.sortChange.next(sort);
  }

  /**
   * Construct an instance of the datasource.
   *
   * @param path The Firebase Path e.g. /items
   * @param db Injectable AngularFireDatabase
   * @param sort Optional initial sort order for the list.
   */
  constructor(
    path: string,
    protected db: AngularFireDatabase,
    sort?: Sort) {
    super();
    this.path = path;
    /**
     * Sets up a subscriber to the path and emits data change events.
     */
    this._sub = this.db.list(this.path).valueChanges<T>()
      .subscribe((data) => {
        this.dataChange.next(data);
      });

    if (sort) {
      this.sort = sort;
    }
  }

  /**
   * Connect to the data source, retrieve initial data, and observe changes.
   * It tracks changes to either the underlying data, or to the sort order and remaps
   * the query.
   *
   * @returns Observable<T[]>
   */
  connect(): Observable<T[]> {
    const dataChanges = [
      this.dataChange,
      this.sortChange
    ];
    const _that = this;

    return Observable.merge(...dataChanges)
      .switchMap(() => {
        if (_that.sort.field !== '' && _that.sort.direction !== '') {
          return this.db.list(this.path, ref => ref.orderByChild(this.sort.field)).valueChanges<T>()
            .map((data: T[]) => {
              if (_that.sort.direction === 'desc') {
                return data.reverse();
              } else {
                return data;
              }
            });
        } else {
          return this.db.list(this.path).valueChanges<T>();
        }
      });
  }

  /**
   * Cleans up the open subscription.
   */
  disconnect() {
    this._sub.unsubscribe();
  }
}

Then an example usage:

roles-datasource.ts Declare this in the 'providers' of the relevant module. (code not shown)

import { FirebaseDataSource } from '../../shared/firebase-datasource';
import { Role } from './role';
import { Injectable } from '@angular/core';
import { AngularFireDatabase } from 'angularfire2/database';


@Injectable()
export class RoleDataSource extends FirebaseDataSource<Role> {
  constructor(
    protected db: AngularFireDatabase
  ) {
    super('/roles', db);
  }
}

Now let's look at the UI component:

all-roles.component.html

Ignoring the extraneous code for toolbars etc. The important parts to notice are the mat-table and matSort directives.

<!-- Toolbar -->
<div class="plr20 mb10 bb-light">
  <div fxLayout="row" fxLayoutAlign="space-between center">
    <h1>All Roles</h1>
    <div>
      <a mat-button [routerLink]="['/roles', 'new']">
        <mat-icon class="cursor-pointer">add</mat-icon>New Role</a>
    </div>
  </div>
</div>
<! -- End Toolbar -->
<div class="plr20" fxLayout="column">
  <div *ngIf="contentLoading" fxLayout="row" fxLayoutAlign="center">
    <div class="spinner-container">
      <mat-spinner diameter="48" strokeWidth="4"></mat-spinner>
    </div>
  </div>
  <mat-card class="mb20">
    <mat-card-content>
      <mat-table #table [dataSource]="dataSource" matSort>

        <!--- Note that these columns can be defined in any order.
                      The actual rendered columns are set as a property on the row definition" -->

        <!-- Identifier Column -->
        <ng-container matColumnDef="identifier">
          <mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
          <mat-cell *matCellDef="let role"> {{role.identifier}} </mat-cell>
        </ng-container>

        <!-- Title Column -->
        <ng-container matColumnDef="title">
          <mat-header-cell *matHeaderCellDef mat-sort-header> Title </mat-header-cell>
          <mat-cell *matCellDef="let role"> {{role.title}} </mat-cell>
        </ng-container>

        <!-- Last Updated -->
        <ng-container matColumnDef="lastUpdated">
          <mat-header-cell *matHeaderCellDef mat-sort-header> Last Updated </mat-header-cell>
          <mat-cell *matCellDef="let role"> {{role.lastUpdated | date}} {{role.lastUpdated | date: 'mediumTime'}} </mat-cell>
        </ng-container>

        <!-- Actions -->
        <ng-container matColumnDef="actions">
          <mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
          <mat-cell *matCellDef="let role"> <a mat-button [routerLink]="['/roles/', role.identifier]">View</a> </mat-cell>
        </ng-container>

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

    </mat-card-content>
  </mat-card>
</div>

all-roles.component.ts

Finally the implementation at UI Layer. As a matter of taste it captures the MatSort updates and emits them to the data source because I don't like binding MatSort directly into the datasource layer. I've also added a simple Ajax Loader while the data is loading.

import { Component, OnDestroy, ViewChild, OnInit } from '@angular/core';
  import { AngularFireDatabase, AngularFireList } from 'angularfire2/database';
  import { Observable } from 'rxjs/Observable';
  import { Subscription } from 'rxjs/Subscription';
  import { Role } from './role';
  import { RoleDataSource } from './role-datasource';
  import { MatSort } from '@angular/material';


  @Component({
    templateUrl: './all-roles.component.html',
    styles: [':host {width: 100% }']
  })
  export class AllRolesComponent implements OnDestroy, OnInit {
    roles: Observable<any>;
    contentLoading: boolean;
    subs: Subscription[] = [];
    displayedColumns = ['identifier', 'title', 'lastUpdated', 'actions'];

    @ViewChild(MatSort) sort: MatSort;

    constructor(private db: AngularFireDatabase, private dataSource: RoleDataSource) {
      this.contentLoading = true;
    }

    ngOnInit() {
      const _that = this;

      // simply handles hiding the AJAX Loader
      this.dataSource.connect().take(1).subscribe(data => {
        this.contentLoading = false;
      });
      this.subs.push(this.sort.sortChange.subscribe(() => {
        _that.dataSource.sort = {
          field: _that.sort.active,
          direction: _that.sort.direction
        };
      }));

    }

    ngOnDestroy() {
      this.subs.forEach((sub) => {
        sub.unsubscribe();
      });
    }

  }
Related