title: Angular Material - Filter Table by multiple filters and multiple filter values at the same time
body: <p>I'm trying to be able to have two different filters for a table of objects.  The first one is a normal text/input based filter and works as expected.</p>
  <p>The second one I'm trying to get working is a row of checkboxes labeled &quot;Level 1&quot;, &quot;Level 2&quot;, etc. On checking a checkbox I want to filter by the column &quot;level&quot; all of the currently checked checkboxes. Ideally the user could filter the table by both the text and level selection</p>
  <p>I've read about using filterPredicate and tried to use <a href="https://stackblitz.com/edit/angular-hbakxo-5jeaic?file=app%2Ftable-filtering-example.ts" rel="nofollow noreferrer">this as a template</a> but I must be missing something.</p>
  <p>Current code snippet:</p>
  <p>HTML:</p>
  <pre><code>//Input for text filter
  &lt;mat-form-field &gt;
    &lt;input matInput (keyup)=&quot;applyFilter($event.target.value)&quot; placeholder=&quot;Filter...&quot;&gt;
  &lt;/mat-form-field&gt;
  
  ...
  
  //Checkboxes for level filter
  &lt;div fxLayout=&quot;row&quot; fxLayoutAlign=&quot;space-between center&quot; class=&quot;margin-1&quot;&gt;
      &lt;mat-checkbox (change)=&quot;customFilterPredicate()&quot; [(ngModel)]=&quot;level.active&quot; *ngFor=&quot;let level of levelsToShow&quot;&gt;{{level.level}}&lt;/mat-checkbox&gt;
  &lt;/div&gt;
  </code></pre>
  <p>TS</p>
  <pre><code>ngOnInit() {
       this.dataSource.filterPredicate = this.customFilterPredicate();
    }
  
  ...
  
  applyFilter(filterValue: string) {
      this.dataSource.filter = filterValue.trim().toLowerCase();
    }
  
  customFilterPredicate() {
      const myFilterPredicate = (data): boolean =&gt; {
        return this.levelsToShow.some(data['level'].toString().trim());
      };
      return myFilterPredicate;
    }
  </code></pre>
  <h2>Update</h2>
  <p>So far I was able to make some progress thanks to Fabian Küng but am still not home free. To clarify I'm hoping to have the text filter be able to search through multiple columns ('castingTime', 'duration', etc),  and have the checkboxes only filter by level.</p>
  <p>As of right now when I click on a checkbox I get this error:
  'Cannot read property 'toLowerCase' of undefined'
  that points to this line of code:</p>
  <pre><code>return data['level'].toString().trim().toLowerCase().indexOf(searchString.name.toLowerCase()) !== -1 &amp;&amp;
  </code></pre>
  <p>but when I console log out data I can see that it has a level property, so I'm not seeing where I'm going wrong.</p>
  <p>Here are the relevant code snippets:</p>
  <pre><code>    HTML:
      
          &lt;input matInput (keyup)=&quot;applyFilter($event.target.value)&quot; placeholder=&quot;Filter...&quot; [formControl]=&quot;textFilter&quot;&gt;
          
          &lt;mat-checkbox (change)=&quot;updateFilter()&quot; [(ngModel)]=&quot;level.active&quot; *ngFor=&quot;let level of levelsToShow&quot;&gt;{{level.name}}&lt;/mat-checkbox&gt;
      
      TS:
      
          levelsToShow: Level[] = [
              { level: '1', active: false, name: 'Level 1' },
              { level: '2', active: false, name: 'Level 2' },
              { level: '3', active: false, name: 'Level 3' },
              { level: '4', active: false, name: 'Level 4' },
              { level: '5', active: false, name: 'Level 5' },
              { level: '6', active: false, name: 'Level 6' },
              { level: '7', active: false, name: 'Level 7' },
              { level: '8', active: false, name: 'Level 8' },
              { level: '9', active: false, name: 'Level 9' }
            ];
          
            levelFilter = new FormControl();
            textFilter = new FormControl();
            globalFilter = '';
          
            filteredValues = {
              level: '', 
              text: '', 
            };
  
  ngOnInit() {
      ...
  
      this.textFilter.valueChanges.subscribe((textFilterValue) =&gt; {
        this.filteredValues['text'] = textFilterValue;
        this.dataSource.filter = JSON.stringify(this.filteredValues);
      });
  
      this.dataSource.filterPredicate = this.customFilterPredicate();
    }
  
    customFilterPredicate() {
      const myFilterPredicate = (data: Spell, filter: string): boolean =&gt; {
        var globalMatch = !this.globalFilter;
  
        if (this.globalFilter) {
          // search all text fields
          globalMatch = data['spellName'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['level'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['castingTime'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['distance'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['details'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['duration'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['school'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['effect'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1;
        }
  
        if (!globalMatch) {
          return;
        }
  
        let searchString = JSON.parse(filter);
  
        return data['level'].toString().trim().toLowerCase().indexOf(searchString.name.toLowerCase()) !== -1 &amp;&amp;
          (this.levelsToShow.filter(level =&gt; !level.active).length === this.levelsToShow.length ||
            this.levelsToShow.filter(level =&gt; level.active).some(level =&gt; level.level === data.level.toString()));
      }
      return myFilterPredicate;
    }
  
    updateFilter() {
      this.dataSource.filter = JSON.stringify(this.filteredValues);
    }
  </code></pre>
  <h2>Update II</h2>
  <p>As requested here is an example of one of the table rows: (this is for a d&amp;d table of spells)</p>
  <pre><code>{
  castingTime: &quot;1 half action&quot;
  distance: &quot;Short range attack&quot;
  duration: &quot;1 round per Casting Level&quot;
  effect: &quot;Once per round, you may take a half action to launch an arrow of acid from your palm, generating a new Spellcasting result to see if you hit.Each arrow inflicts 1d6 acid damage.↵  &quot;
  index: 0
  level: &quot;4&quot;
  school: &quot;Creation&quot;
  spellName: &quot;ACID ARROW&quot;
  }
  </code></pre>
  <h2>Final Update</h2>
  <p>This is what I ended up with in case anyone gets stuck. Thanks to Fabian Küng for figuring all this out! The only weird thing of note is that I ended up using the actual value of the text input &quot;textFilter&quot; because stringifying the text filteredValues and then parsing them (the filter only accepts strings as far as I can tell) kept giving me &quot;JSON error, unexpected value at 0&quot; messages and as far as I can tell this will work fine EXCEPT now I need to figure out how to throttle the filter.</p>
  <pre><code>customFilterPredicate() {
      const myFilterPredicate = (data: Spell, filter: string): boolean =&gt; {
        var globalMatch = !this.globalFilter;
  
        if (this.globalFilter) {
          // search all text fields
          globalMatch = data['spellName'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['level'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['castingTime'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['distance'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['details'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['duration'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['school'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1 ||
                        data['effect'].toString().trim().toLowerCase().indexOf(this.globalFilter.toLowerCase()) !== -1;
        }
  
        if (!globalMatch) {
          return;
        }
  
        return data.spellName.toString().trim().toLowerCase().indexOf(this.textFilter.value) !== -1 &amp;&amp;
          (this.levelsToShow.filter(level =&gt; !level.active).length === this.levelsToShow.length ||
            this.levelsToShow.filter(level =&gt; level.active).some(level =&gt; level.level === data.level.toString()));
      }
      return myFilterPredicate;
    }
  </code></pre>