0
votes

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.

example.ts:

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<string>(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(() => {
          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 => this.selection.select(data.id));
    } else {
      this.exampleDatabase.data.forEach(data => 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<UserData[]> = new BehaviorSubject<UserData[]>([]);
  get data(): UserData[] { return this.dataChange.value; }

  constructor() {
    // Fill up the database with 100 users.
    for (let i = 0; i < 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<any> {
  _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(() => this._paginator.pageIndex = 0);
  }

  /** Connect function called by the table to retrieve one stream containing the data to render. */
  connect(): Observable<UserData[]> {
    // 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(() => {
      // Filter data
      this.filteredData = this._exampleDatabase.data.slice().filter((item: UserData) => {
        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) => {
      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 < valueB ? -1 : 1) * (this._sort.direction == 'asc' ? 1 : -1);
    });
  }
}

example.html:

<div class="example-header" [style.display]="selection.isEmpty() ? '' : 'none'">
  <mat-form-field floatPlaceholder="never">
    <input matInput #filter placeholder="Filter users">
  </mat-form-field>
</div>
<div class="example-header example-selection-header"
     *ngIf="!selection.isEmpty()">
  {{selection.selected.length}}
  {{selection.selected.length == 1 ? 'user' : 'users'}}
  selected
</div>

<div class="example-container mat-elevation-z8">

  <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" -->

    <!-- Checkbox Column -->
    <ng-container matColumnDef="select">
      <mat-header-cell *matHeaderCellDef>
        <mat-checkbox (change)="$event ? masterToggle() : null"
                     [checked]="isAllSelected()"
                     [indeterminate]="selection.hasValue() && !isAllSelected()">
        </mat-checkbox>
      </mat-header-cell>
      <mat-cell *matCellDef="let row">
        <mat-checkbox (click)="$event.stopPropagation()"
                     (change)="$event ? selection.toggle(row.id) : null"
                     [checked]="selection.isSelected(row.id)">
        </mat-checkbox>
      </mat-cell>
    </ng-container>

    <!-- ID Column -->
    <ng-container matColumnDef="userId">
      <mat-header-cell *matHeaderCellDef mat-sort-header> ID </mat-header-cell>
      <mat-cell *matCellDef="let row"> {{row.id}} </mat-cell>
    </ng-container>

    <!-- Progress Column -->
    <ng-container matColumnDef="progress">
      <mat-header-cell *matHeaderCellDef mat-sort-header> Progress </mat-header-cell>
      <mat-cell *matCellDef="let row"> {{row.progress}}% </mat-cell>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="userName">
      <mat-header-cell *matHeaderCellDef mat-sort-header> Name </mat-header-cell>
      <mat-cell *matCellDef="let row"> {{row.name}} </mat-cell>
    </ng-container>

    <!-- Color Column -->
    <ng-container matColumnDef="color">
      <mat-header-cell *matHeaderCellDef mat-sort-header> Color </mat-header-cell>
      <mat-cell *matCellDef="let row" [style.color]="row.color"> {{row.color}} </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"
            [class.example-selected-row]="selection.isSelected(row.id)"
            (click)="selection.toggle(row.id)">
    </mat-row>
  </mat-table>

  <div class="example-no-results"
       [style.display]="dataSource.renderedData.length == 0 ? '' : 'none'">
    No users found matching filter.
  </div>

  <mat-paginator #paginator
                [length]="dataSource.filteredData.length"
                [pageIndex]="0"
                [pageSize]="25"
                [pageSizeOptions]="[5, 10, 25, 100]">
  </mat-paginator>
</div>
1

1 Answers

2
votes

Try refactoring and placing the filter when the data is sorted, this is how I got filtering, pagination and sorting to work. You will get errors until you refactor the rest of your code to work with the refactored data source class:

    export class ExampleDataSource extends DataSource<any> {

          _filterChange = new BehaviorSubject('');
          get filter(): string { return this._filterChange.value; }
          set filter(filter: string) { this._filterChange.next(filter); }

          renderedData: UserData[] = [];

            constructor(private _exampleDatabase: ExampleDatabase,
                        private _paginator: MatPaginator,
                        private _sort: MatSort) {
                super();
            }

            connect(): Observable<Data[]> {
                const displayDataChanges = [
                    this._exampleDatabase.dataChange,
                    this._paginator.page,
                    this._sort.sortChange,
                    this._filterChange
                ];

                return Observable.merge(...displayDataChanges).map(() => {
                    const startIndex = this._paginator.pageIndex * this._paginator.pageSize;
                    this.renderedData= this.sortData();
                    return this.renderedData.splice(startIndex, this._paginator.pageSize);
                });
            }

            disconnect() {}

            sortData(): Data[] {
                const data = this._exampleDatabase.data.slice().filter((item: Data) => {        
                        let searchStr = (item.name + item.color).toLowerCase();             
                        return searchStr.indexOf(this.filter.toLowerCase()) != -1;
                });

                if (!this._sort.active || this._sort.direction == '') { return data; }

                return data.sort((a, b) => {
                  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 < valueB ? -1 : 1) * (this._sort.direction == 'asc' ? 1 : -1);
                });
            }
}