Angular Custom Table Component Filtering

Home / Angular Custom Table Component Filtering

Continuing with more small features for my custom table component, I wanted to utilize what I implemented for multiple key filter with pipes for generic filtering. While implementing this, I learned more about observables, events, and sharing data across components.


The table component expects a list of columns. The interesting thing about these columns is that everything about a column is defined and bound text/values in each column’s cell can be “calculated.” As such, I had to modify the original multiple key pipe a bit. I also played with a stand-alone filter component that uses the pipe to decouple data updates and filter.

Let’s look at the filter component first.

@Component({
    selector: 'filter',
    templateUrl: 'templates/filter.html',
    pipes: [ CustomFilterPipe ] 
})

export class Filter implements OnInit, ControlValueAccessor {
  public filterText: string;
  public filterPlaceholder: string;
  public filterPipe: CustomFilterPipe = new CustomFilterPipe();
  public filterInput = new FormControl();
  private subscription: Subscription;
  public filteredData: Array<any>;
  @Input() public options: CustomTableOptions;
  @Output() filterChange: EventEmitter<Array<any>> = new EventEmitter<Array<any>>();

So far, so good. You can see that the filter will take an input using the CustomTableOptions type. I expect this to contain all of the column definitions. Why is this necessary? Well, since the table cells can contain evaluated/calculated values, the pipe must be able to calculate these values for comparison as well. You can also see that there is a EventEmitter called “filterChange.” This will be used to alert the parent component when there is new filtered data available (Array<any>). Note that an instance of the “CustomerFilterPipe” is being instantiated as well.

  constructor(private _elRef: ElementRef, private _renderer: Renderer,
    private _changeDetectorRef: ChangeDetectorRef) {
  }
  
  ngOnInit() {
    this.subscription = this.options.records.subscribe(res => { 
      this.filteredData = res;
    });
    this.filterText = "";
    this.filterPlaceholder = "Filter..";
    this.filterInput
      .valueChanges
      .debounceTime(200)
      .distinctUntilChanged()
      .subscribe(term => {
        this.filterText = term;
        var arr = this.filterPipe.transform(this.filteredData, this.options.columns, this.filterText, false);
        this.filterChange.emit(arr);
      });
  }
}

You can see that we’re subscribing to the records being passed on the options (observable) so that the parent can change its data source as needed. We’re also using a similar mechanism that was used in the multiselect component to detect and debounce changes on the filter text input. A change to the filter text triggers the event emitter which the parent component can utilize. The filtered array of data is passed into the emitter.

And here’s the component’s template:

<div class="form-group">
  <input class="form-control" type="text" [value]="filterText" [placeholder]="filterPlaceholder"
    [formControl]="filterInput" />
</div>

The custom pipe looks very similar to the multiple key filter pipe I previously mentioned. However, it did require parsing the column values. I also changed the signature to take an array of columns to compare against rather than an object and uses its keys.

@Pipe({
    name: 'customFilter'
})

export class CustomFilterPipe implements PipeTransform {
  
  getCellValue(row: any, column: CustomTableColumnDefinition): string {
    if (column.isComputed) {
      let evalfunc = new Function ('r', 'return ' + column.binding);
      let evalresult:string = evalfunc(row);
      return evalresult;
    } else {
      return column.binding.split('.').reduce((prev:any, curr:string) => prev[curr], row);
    }
  }
  
  transform(items: any, columns: any, filterText: string, isAnd: bool): any {
    if (columns && Array.isArray(items)) {
      if (isAnd) {
        return items.filter(item =>
            columns.reduce((acc, column) => {
              var evalResult: string = this.getCellValue(item, column);
              var isMatch = new RegExp(filterText, 'gi').test(evalResult) || filterText === "";
              return acc && isMatch;
            }, true));
      } else {
        return items.filter(item => {
          return columns.some((column) => {
            var evalResult: string = this.getCellValue(item, column);
            var isMatch = new RegExp(filterText, 'gi').test(evalResult) || filterText === "";
            return isMatch;
          });
        });
      }
    } else {
      return items;
    }
  }
}

With the filter in place, most everything is done. This is where things were interesting to me. Previously, I simply built up a list of records and used Observable.from(array) to pass into the table component. I thought that I could simply update the records through the EventEmitter and that table component would merrily be updated.

This is not the case. Observables, like the one I created, are “cold.” That is to say, their subscribers will be notified once through their subscription and that’s, effectively, the end of the transaction. Any future changes to the underlying data that is being observed does not trigger subsequent subscriptions. RxJS has a mechanism to deal with this called Subjects. Subjects, themselves, are observables.

In the parent (route) component, I changed the observable to use a Subject.

private tableSubject: BehaviorSubject<Array<any>> = new BehaviorSubject([]);
public tableObserve: Observable<Array<any>> = this.tableSubject.asObservable();

private filterSubject: BehaviorSubject<Array<any>> = new BehaviorSubject([]);
public filterObserve: Observable<Array<any>> = this.filterSubject.asObservable();

By creating an observable that observbes a subject, we create a “hot” observable. With a hot observable, we can push changes to subscribers whenever our underlying data changes. The reason I use a BehaviorSubject is so that whenever a subscriber subscribes to the observable, it will get the last published/pushed value. Otherwise, if it didn’t, there would be lots of timing issues and data could get missed by the subscriber; especially on init. The regular Subject does not implement this behavior.

Back to the filter, we pass the subject into our options for both the table and the filter components:

this.tableOptions = {
  records: this.tableSubject,
  columns: columns
};

this.filterOptions = {
  records: this.filterSubject,
  columns: columns
};

The subject has a “next” method that will push changes to subscribers. I’ve defined two methods to control pushing changes to the table or to the filters. With the filter, in my demo below, I’m only ever updating once. These methods are called in the initialization of the route component.

pushChange() {
  this.tableSubject.next(this.records);
}

pushFilterData() {
  this.filterSubject.next(this.records);
}

Finally, the route component has to handle the “filterChange” EventEmitter.

Note that I’m replacing “this.records.” This is probably fine in most cases since the filter will have a handle to the original data. Of course, an intermediate array could be used to avoid modification of the original data.

filterChange($event) {
  this.records = $event;
  this.pushChange();
}

The “hot” observables also illustrate how the table and filter could easily receive data from an API endpoint without any modification to the underlying components. It reminds me a lot of the v1.x $watch features, but the onus is put on the keeper of the data. Surely, this improves performance…

The only change that had to be made to the table component was to mark it for change when it receives new data on its subscription to the BehaviorSubject. I also left in some commented out code that illustrates how you achieve a UI refresh via zone.run:

ngOnInit() {
  this._subscription = this.options.records.subscribe(res => { 
    this.filteredDataObservable = Observable.of(res); 
    this.filteredData = res;
    this.changeRef.markForCheck();
    //this.zone = new NgZone({enableLongStackTrace: false});
    //this.zone.run(() => {
    //  console.log('Received table data');
    //});
  });

I was pleased with how this bit of code turned out. It illustrates a lot of concepts like sharing data, reactive design, and events between components. It’s often hard to find examples of everything put together.

The plunk below shows everything in action.

One thought on “Angular Custom Table Component Filtering”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.