title: Angular Material Data Table with filtering using Firestore
body: <p>This is a material data table which uses an example database to populate it.  It also incorporates sorting, pagination, and filtering.  I have already managed to populate a table with my Firestore data (coming from a service) with sorting and pagination functionality.  However I can not get the filtering to work and I can't figure out why.  I know it's a long shot but I'm hoping someone can show me how to implement a Firestore database in place of the example database below.</p>
  
  <p>example.ts:</p>
  
  <pre class="lang-ts prettyprint-override"><code>import {Component, ElementRef, ViewChild} from '@angular/core';
  import {DataSource} from '@angular/cdk/collections';
  import {MatPaginator, MatSort} from '@angular/material';
  import {SelectionModel} from '@angular/cdk/collections';
  import {BehaviorSubject} from 'rxjs/BehaviorSubject';
  import {Observable} from 'rxjs/Observable';
  import 'rxjs/add/operator/startWith';
  import 'rxjs/add/observable/merge';
  import 'rxjs/add/observable/fromEvent';
  import 'rxjs/add/operator/map';
  import 'rxjs/add/operator/distinctUntilChanged';
  import 'rxjs/add/operator/debounceTime';
  
  /**
   * @title Feature-rich data table
   */
  @Component({
    selector: 'table-overview-example',
    styleUrls: ['table-overview-example.css'],
    templateUrl: 'table-overview-example.html',
  })
  export class TableOverviewExample {
    displayedColumns = ['select', 'userId', 'userName', 'progress', 'color'];
    exampleDatabase = new ExampleDatabase();
    selection = new SelectionModel&lt;string&gt;(true, []);
    dataSource: ExampleDataSource | null;
  
    @ViewChild(MatPaginator) paginator: MatPaginator;
    @ViewChild(MatSort) sort: MatSort;
    @ViewChild('filter') filter: ElementRef;
  
    ngOnInit() {
      this.dataSource = new ExampleDataSource(this.exampleDatabase, this.paginator, this.sort);
      Observable.fromEvent(this.filter.nativeElement, 'keyup')
          .debounceTime(150)
          .distinctUntilChanged()
          .subscribe(() =&gt; {
            if (!this.dataSource) { return; }
            this.dataSource.filter = this.filter.nativeElement.value;
          });
    }
  
    isAllSelected(): boolean {
      if (!this.dataSource) { return false; }
      if (this.selection.isEmpty()) { return false; }
  
      if (this.filter.nativeElement.value) {
        return this.selection.selected.length == this.dataSource.renderedData.length;
      } else {
        return this.selection.selected.length == this.exampleDatabase.data.length;
      }
    }
  
    masterToggle() {
      if (!this.dataSource) { return; }
  
      if (this.isAllSelected()) {
        this.selection.clear();
      } else if (this.filter.nativeElement.value) {
        this.dataSource.renderedData.forEach(data =&gt; this.selection.select(data.id));
      } else {
        this.exampleDatabase.data.forEach(data =&gt; this.selection.select(data.id));
      }
    }
  }
  
  /** Constants used to fill up our data base. */
  const COLORS = ['maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'purple',
    'fuchsia', 'lime', 'teal', 'aqua', 'blue', 'navy', 'black', 'gray'];
  const NAMES = ['Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack',
    'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper',
    'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas', 'Elizabeth'];
  
  export interface UserData {
    id: string;
    name: string;
    progress: string;
    color: string;
  }
  
  /** An example database that the data source uses to retrieve data for the table. */
  export class ExampleDatabase {
    /** Stream that emits whenever the data has been modified. */
    dataChange: BehaviorSubject&lt;UserData[]&gt; = new BehaviorSubject&lt;UserData[]&gt;([]);
    get data(): UserData[] { return this.dataChange.value; }
  
    constructor() {
      // Fill up the database with 100 users.
      for (let i = 0; i &lt; 100; i++) { this.addUser(); }
    }
  
    /** Adds a new user to the database. */
    addUser() {
      const copiedData = this.data.slice();
      copiedData.push(this.createNewUser());
      this.dataChange.next(copiedData);
    }
  
    /** Builds and returns a new User. */
    private createNewUser() {
      const name =
          NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' +
          NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.';
  
      return {
        id: (this.data.length + 1).toString(),
        name: name,
        progress: Math.round(Math.random() * 100).toString(),
        color: COLORS[Math.round(Math.random() * (COLORS.length - 1))]
      };
    }
  }
  
  /**
   * Data source to provide what data should be rendered in the table. Note that the data source
   * can retrieve its data in any way. In this case, the data source is provided a reference
   * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage
   * the underlying data. Instead, it only needs to take the data and send the table exactly what
   * should be rendered.
   */
  export class ExampleDataSource extends DataSource&lt;any&gt; {
    _filterChange = new BehaviorSubject('');
    get filter(): string { return this._filterChange.value; }
    set filter(filter: string) { this._filterChange.next(filter); }
  
    filteredData: UserData[] = [];
    renderedData: UserData[] = [];
  
    constructor(private _exampleDatabase: ExampleDatabase,
                private _paginator: MatPaginator,
                private _sort: MatSort) {
      super();
  
      // Reset to the first page when the user changes the filter.
      this._filterChange.subscribe(() =&gt; this._paginator.pageIndex = 0);
    }
  
    /** Connect function called by the table to retrieve one stream containing the data to render. */
    connect(): Observable&lt;UserData[]&gt; {
      // Listen for any changes in the base data, sorting, filtering, or pagination
      const displayDataChanges = [
        this._exampleDatabase.dataChange,
        this._sort.sortChange,
        this._filterChange,
        this._paginator.page,
      ];
  
      return Observable.merge(...displayDataChanges).map(() =&gt; {
        // Filter data
        this.filteredData = this._exampleDatabase.data.slice().filter((item: UserData) =&gt; {
          let searchStr = (item.name + item.color).toLowerCase();
          return searchStr.indexOf(this.filter.toLowerCase()) != -1;
        });
  
        // Sort filtered data
        const sortedData = this.sortData(this.filteredData.slice());
  
        // Grab the page's slice of the filtered sorted data.
        const startIndex = this._paginator.pageIndex * this._paginator.pageSize;
        this.renderedData = sortedData.splice(startIndex, this._paginator.pageSize);
        return this.renderedData;
      });
    }
  
    disconnect() {}
  
    /** Returns a sorted copy of the database data. */
    sortData(data: UserData[]): UserData[] {
      if (!this._sort.active || this._sort.direction == '') { return data; }
  
      return data.sort((a, b) =&gt; {
        let propertyA: number|string = '';
        let propertyB: number|string = '';
  
        switch (this._sort.active) {
          case 'userId': [propertyA, propertyB] = [a.id, b.id]; break;
          case 'userName': [propertyA, propertyB] = [a.name, b.name]; break;
          case 'progress': [propertyA, propertyB] = [a.progress, b.progress]; break;
          case 'color': [propertyA, propertyB] = [a.color, b.color]; break;
        }
  
        let valueA = isNaN(+propertyA) ? propertyA : +propertyA;
        let valueB = isNaN(+propertyB) ? propertyB : +propertyB;
  
        return (valueA &lt; valueB ? -1 : 1) * (this._sort.direction == 'asc' ? 1 : -1);
      });
    }
  }
  </code></pre>
  
  <p>example.html:</p>
  
  <pre class="lang-html prettyprint-override"><code>&lt;div class="example-header" [style.display]="selection.isEmpty() ? '' : 'none'"&gt;
    &lt;mat-form-field floatPlaceholder="never"&gt;
      &lt;input matInput #filter placeholder="Filter users"&gt;
    &lt;/mat-form-field&gt;
  &lt;/div&gt;
  &lt;div class="example-header example-selection-header"
       *ngIf="!selection.isEmpty()"&gt;
    {{selection.selected.length}}
    {{selection.selected.length == 1 ? 'user' : 'users'}}
    selected
  &lt;/div&gt;
  
  &lt;div class="example-container mat-elevation-z8"&gt;
  
    &lt;mat-table #table [dataSource]="dataSource" matSort&gt;
  
      &lt;!--- Note that these columns can be defined in any order.
            The actual rendered columns are set as a property on the row definition" --&gt;
  
      &lt;!-- Checkbox Column --&gt;
      &lt;ng-container matColumnDef="select"&gt;
        &lt;mat-header-cell *matHeaderCellDef&gt;
          &lt;mat-checkbox (change)="$event ? masterToggle() : null"
                       [checked]="isAllSelected()"
                       [indeterminate]="selection.hasValue() &amp;&amp; !isAllSelected()"&gt;
          &lt;/mat-checkbox&gt;
        &lt;/mat-header-cell&gt;
        &lt;mat-cell *matCellDef="let row"&gt;
          &lt;mat-checkbox (click)="$event.stopPropagation()"
                       (change)="$event ? selection.toggle(row.id) : null"
                       [checked]="selection.isSelected(row.id)"&gt;
          &lt;/mat-checkbox&gt;
        &lt;/mat-cell&gt;
      &lt;/ng-container&gt;
  
      &lt;!-- ID Column --&gt;
      &lt;ng-container matColumnDef="userId"&gt;
        &lt;mat-header-cell *matHeaderCellDef mat-sort-header&gt; ID &lt;/mat-header-cell&gt;
        &lt;mat-cell *matCellDef="let row"&gt; {{row.id}} &lt;/mat-cell&gt;
      &lt;/ng-container&gt;
  
      &lt;!-- Progress Column --&gt;
      &lt;ng-container matColumnDef="progress"&gt;
        &lt;mat-header-cell *matHeaderCellDef mat-sort-header&gt; Progress &lt;/mat-header-cell&gt;
        &lt;mat-cell *matCellDef="let row"&gt; {{row.progress}}% &lt;/mat-cell&gt;
      &lt;/ng-container&gt;
  
      &lt;!-- Name Column --&gt;
      &lt;ng-container matColumnDef="userName"&gt;
        &lt;mat-header-cell *matHeaderCellDef mat-sort-header&gt; Name &lt;/mat-header-cell&gt;
        &lt;mat-cell *matCellDef="let row"&gt; {{row.name}} &lt;/mat-cell&gt;
      &lt;/ng-container&gt;
  
      &lt;!-- Color Column --&gt;
      &lt;ng-container matColumnDef="color"&gt;
        &lt;mat-header-cell *matHeaderCellDef mat-sort-header&gt; Color &lt;/mat-header-cell&gt;
        &lt;mat-cell *matCellDef="let row" [style.color]="row.color"&gt; {{row.color}} &lt;/mat-cell&gt;
      &lt;/ng-container&gt;
  
      &lt;mat-header-row *matHeaderRowDef="displayedColumns"&gt;&lt;/mat-header-row&gt;
      &lt;mat-row *matRowDef="let row; columns: displayedColumns;"
              [class.example-selected-row]="selection.isSelected(row.id)"
              (click)="selection.toggle(row.id)"&gt;
      &lt;/mat-row&gt;
    &lt;/mat-table&gt;
  
    &lt;div class="example-no-results"
         [style.display]="dataSource.renderedData.length == 0 ? '' : 'none'"&gt;
      No users found matching filter.
    &lt;/div&gt;
  
    &lt;mat-paginator #paginator
                  [length]="dataSource.filteredData.length"
                  [pageIndex]="0"
                  [pageSize]="25"
                  [pageSizeOptions]="[5, 10, 25, 100]"&gt;
    &lt;/mat-paginator&gt;
  &lt;/div&gt;
  </code></pre>