0
votes

I've read the various implementations of filterPredicate on SO, Github, etc but they aren't helpful for me to understand what to do with type ahead searches.

I enter a letter into an input form field, say p, and I receive all the data with last names starting with p from the db. That part of my setup works fine. However, I don't want to hit the db again when I type the next letter, say r. I want to filter the data table for last names starting with pr. This is where the trouble starts.

When I type the second letter I have an if/else statement that tests if the var I'm using has >1 in the string. When it does I pass params to a function for the custom filtering on the table with the data already downloaded from the db. I'm avoiding a db call with every letter, which does work. I don't understand "(data, filter)". They seem like params but aren't. How do they work? What code is needed to finish this?

(I have `dataSource.filter = filterValue; working fine elsewhere.)

Params explained:

column = user_name
filterValue = pr...

The confusion:

public filterColumn(column: string, filterValue: string, dataSource) {

    dataSource.filterPredicate = (data, filter) => {
      console.log('data in filter column: ', data);  // Never called.
      // What goes here?
      // return ???;
    }
  }

My dataSource object. I see filterPredicate, data, and filter properties to work with. Rather abstract how to use them.

dataSource in filterColumn:  MatTableDataSource {_renderData: BehaviorSubject, _filter: BehaviorSubject, _internalPageChanges: Subject, _renderChangesSubscription: Subscriber, sortingDataAccessor: ƒ, …}
filterPredicate: (data, filter) => {…}arguments: [Exception: TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
    at Function.invokeGetter (<anonymous>:2:14)]caller: (...)length: 2name: ""__proto__: ƒ ()[[FunctionLocation]]: data-utilities.service.ts:43[[Scopes]]: Scopes[3]
filteredData: (3) [{…}, {…}, {…}]
sortData: (data, sort) => {…}
sortingDataAccessor: (data, sortHeaderId) => {…}
_data: BehaviorSubject {_isScalar: false, observers: Array(1), closed: false, isStopped: false, hasError: false, …}
_filter: BehaviorSubject {_isScalar: false, observers: Array(1), closed: false, isStopped: false, hasError: false, …}
_internalPageChanges: Subject {_isScalar: false, observers: Array(1), closed: false, isStopped: false, hasError: false, …}
_paginator: MatPaginator {_isInitialized: true, _pendingSubscribers: null, initialized: Observable, _disabled: false, _intl: MatPaginatorIntl, …}
_renderChangesSubscription: Subscriber {closed: false, _parentOrParents: null, _subscriptions: Array(1), syncErrorValue: null, syncErrorThrown: false, …}
_renderData: BehaviorSubject {_isScalar: false, observers: Array(1), closed: false, isStopped: false, hasError: false, …}data: (...)filter: (...)paginator: (...)sort: (...)__proto__: DataSource
1

1 Answers

0
votes

I've included most of the component I made in Angular for typeahead search. The guts of the typeahead code is in the utilities shared component at the bottom. I used a shared component here because I'll use this in many places. However, I think it is a hack and a more elegant answer is possible. This works, it is easy, but not all that pretty. I can't afford more time to figure out pretty now. I suspect the answer is in RegEx.

In the typeahead.compoent in the .pipe you'll find how I call the code in the utility.

This code is in a shared component typeahead.component.ts

public searchLastName$ = new Subject<string>(); // Binds to the html text box element.

ngAfterViewInit() {

// -------- For Column Incremental Queries --------- //
  // searchLastName$ binds to the html element.

    this.searchLastName$.subscribe(result => {
         this.queryLastName(result);
    });
}


//  --------- LAST NAME INCREMENTAL QUERY --------------- //

private queryLastName(filterValue) {

    // Custom filter for this function.  If in ngOnInit on the calling component then it applies 
    // to the whole calling component.  We need various filters so that doesn't work.

    this.membersComponent.dataSource.filterPredicate = (data: { last_name: string }, filterValue: string) =>
        data.last_name.trim().toLowerCase().indexOf(filterValue) !== -1;

    //  When the first letter is typed then get data from db.  After that just filter the table.
    if (filterValue.length === 1) {

      filterValue = filterValue.trim(); // Remove whitespace
      // filterValue = filterValue.toUpperCase(); // MatTableDataSource defaults to lowercase matches

      const lastNameSearch = gql`
        query ($last_name: String!) {
            lastNameSearch(last_name: $last_name) {
                ...membersTableFrag
            }
        }
        ${membersTableFrag}
      `;

      this.apollo
          .watchQuery({
              query: lastNameSearch,
              variables: {
                  last_name: filterValue,
              },
          })
          .valueChanges
          .pipe(
              map(returnedArray => {
                  // console.log('returnedArray  in map: ', returnedArray); // All last_name's with the letter in them someplace.
                  const membersArray = returnedArray.data['lastNameSearch'];  // extract items array from GraphQL JSON array

                  // For case insensitive search
                  const newArray = membersArray.filter(this.utilitiesService.filterBy(filterValue, 'last_name'));

                  return newArray;
              })
          )
          .subscribe(result => {
              this.membersComponent.dataSource.data = result;
          });

    } else {
        // Filter the table instead of calling the db for each letter entered.
        // Note:  Apollo client doesn't seem able to query the cache with this kind of search.

        filterValue = filterValue.trim(); // Remove whitespace
        filterValue = filterValue.toLowerCase(); // MatTableDataSource defaults to lowercase matches

        // Interface and redefinition of filterPredicate in the ngOnInit

        this.membersComponent.dataSource.filter = filterValue;  // Filters all columns unless modifed by filterPredicate.

    }
}

utilities.service.ts

// -------------- DATABASE COLUMN SEARCH -------------
  // Shared with other components with tables.
  // For case insensitive search.

  // THIS NEEDS TO BE CLEANED UP BUT I'M MOVING ON, MAYBE LATER

  public filterBy = (filterValue, column) => {

    return (item) => {
      const charTest = item[column].charAt(0);
      if (charTest === filterValue.toLowerCase()) {
        return true;
      } else if (charTest === filterValue.toUpperCase()) {
        return true;
      } else {
        return false;
      }
    }
  };