Reputation: 1967
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.
The second one I'm trying to get working is a row of checkboxes labeled "Level 1", "Level 2", etc. On checking a checkbox I want to filter by the column "level" all of the currently checked checkboxes. Ideally the user could filter the table by both the text and level selection
I've read about using filterPredicate and tried to use this as a template but I must be missing something.
Current code snippet:
HTML:
//Input for text filter
<mat-form-field >
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter...">
</mat-form-field>
...
//Checkboxes for level filter
<div fxLayout="row" fxLayoutAlign="space-between center" class="margin-1">
<mat-checkbox (change)="customFilterPredicate()" [(ngModel)]="level.active" *ngFor="let level of levelsToShow">{{level.level}}</mat-checkbox>
</div>
TS
ngOnInit() {
this.dataSource.filterPredicate = this.customFilterPredicate();
}
...
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
customFilterPredicate() {
const myFilterPredicate = (data): boolean => {
return this.levelsToShow.some(data['level'].toString().trim());
};
return myFilterPredicate;
}
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.
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:
return data['level'].toString().trim().toLowerCase().indexOf(searchString.name.toLowerCase()) !== -1 &&
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.
Here are the relevant code snippets:
HTML:
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter..." [formControl]="textFilter">
<mat-checkbox (change)="updateFilter()" [(ngModel)]="level.active" *ngFor="let level of levelsToShow">{{level.name}}</mat-checkbox>
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) => {
this.filteredValues['text'] = textFilterValue;
this.dataSource.filter = JSON.stringify(this.filteredValues);
});
this.dataSource.filterPredicate = this.customFilterPredicate();
}
customFilterPredicate() {
const myFilterPredicate = (data: Spell, filter: string): boolean => {
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 &&
(this.levelsToShow.filter(level => !level.active).length === this.levelsToShow.length ||
this.levelsToShow.filter(level => level.active).some(level => level.level === data.level.toString()));
}
return myFilterPredicate;
}
updateFilter() {
this.dataSource.filter = JSON.stringify(this.filteredValues);
}
As requested here is an example of one of the table rows: (this is for a d&d table of spells)
{
castingTime: "1 half action"
distance: "Short range attack"
duration: "1 round per Casting Level"
effect: "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.↵ "
index: 0
level: "4"
school: "Creation"
spellName: "ACID ARROW"
}
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 "textFilter" because stringifying the text filteredValues and then parsing them (the filter only accepts strings as far as I can tell) kept giving me "JSON error, unexpected value at 0" messages and as far as I can tell this will work fine EXCEPT now I need to figure out how to throttle the filter.
customFilterPredicate() {
const myFilterPredicate = (data: Spell, filter: string): boolean => {
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 &&
(this.levelsToShow.filter(level => !level.active).length === this.levelsToShow.length ||
this.levelsToShow.filter(level => level.active).some(level => level.level === data.level.toString()));
}
return myFilterPredicate;
}
Upvotes: 7
Views: 18158
Reputation: 711
Use FilterPredicate in datasource to set the multiple expressions
Use Form Group to assign controls that will send filterValue
<form [formGroup]="filterForm">
<mat-form-field>
<input matInput placeholder="Model" formControlName="model" />
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Type" formControlName="modelType" />
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Status</mat-label>
<mat-select formControlName="status">
<mat-option value=""></mat-option>
<mat-option value="Active">Active</mat-option>
<mat-option value="InActive">InActive</mat-option>
</mat-select>
</mat-form-field>
#TypeScript Specify the filterPredicate expression
// Filter Predicate Expression to perform Multiple KeyWord Search
this.dataSource.filterPredicate = ((data: DeviceInventory, filter) => {
const a = !filter.model || data.modelCode.trim().toLowerCase().includes(filter.model);
const b = !filter.modelType || data.modelType.trim().toLowerCase().includes(filter.modelType);
return a && b;
}) as (DeviceInventory, string) => boolean;
Subscribe to FomrGroup Value changes to listen to filterValues
this.filterForm.valueChanges.subscribe(value => {
this.dataSource.filter = value;
console.log(value);
});
}
Upvotes: 0
Reputation: 6183
You were on the right track with setting up the checkboxes and then using the model of the checkboxes to filter in the customFilterPredicate
function.
I took the linked stackblitz and modified it to make it work with your checkboxes, check it here.
In the template you have your checkboxes like you set it up:
<mat-checkbox (change)="updateFilter()" [(ngModel)]="level.active" *ngFor="let level of levelsToShow">{{level.name}}</mat-checkbox>
In the component I added a Level
interface:
export interface Level {
active: boolean;
name: string;
level: number;
}
and some data:
levelsToShow: Level[] = [
{ level: 1, active: false, name: 'Level 1' },
{ level: 2, active: false, name: 'Level 2' },
{ level: 3, active: false, name: 'Level 3' },
];
And the most important part is in the customFilterPredicate
function. What it does is, it filters the name
property in the first condition and in the second one it checks if all levels are set to false
, in this case we return true
as I assume you want to see all levels if no checkbox is checked. If a level is active, we filter by that level, or multiple levels if multiple are selected.
return data.name.toString().trim().toLowerCase().indexOf(searchString.name.toLowerCase()) !== -1 &&
(this.levelsToShow.filter(level => !level.active).length === this.levelsToShow.length ||
this.levelsToShow.filter(level => level.active).some(level => level.level === data.level));
A small but important part is to actually trigger the filtering when you check/uncheck a checkbox. This can be done by just setting the datasource filter
again and is called whenever a checkbox value changes:
updateFilter() {
this.dataSource.filter = JSON.stringify(this.filteredValues);
}
Upvotes: 8