I think I would add my approach to anyone looking for this solution.
I tried to make it reusable in collections. It supports data retrieval and sorting up and down by specified fields. Remember to add sort fields to .indexOn in firebase rules.
I was unable to get the paging to work because it is too hard to work out startKey!
firebase-datasource.ts
Let's start by defining a template data source that I can reuse for all collections that require it.
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'; export interface Sort { field: string; direction: '' | 'asc' | 'desc'; } export class FirebaseDataSource<T> extends DataSource<T> { 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. eg /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 eg /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(): 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>(); } }); } disconnect() { this._sub.unsubscribe(); } }
Then an example of use:
Role-playing datasource.ts Declare this in the "providers" of the corresponding 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 look at the user interface component:
all-roles.component.html
Ignoring external code for toolbars, etc. Important parts for notification are the mat-table and matSort .
<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> <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> <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> <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> <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, a user interface level implementation. It captures matSort updates to its taste and emits them to the data source, because I don't like to bind MatSort directly to the data source layer. I also added a simple Ajax loader during data 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;