I'm using mat-tree angular material component. It's a nice component with some very useful features like, multi-select, expand all/collapse all. I was not able to find any tree filtering feature in any of their APIs. Has anyone came across this feature or done any work around to get mat-tree filter?
6 Answers
After I have spent several days on the same task here are some tips i can give: I am using input event to follow the user input:
<input matInput class="form-control"
(input)="filterChanged($event.target.value)"
placeholder="Search Skill">
On this filter I attached a subject so i can subscribe to it:
searchFilter: Subject<string> = new Subject<string>();
filterChanged(filter: string): void {
this.searchFilter.next(filter);
}
To make it smooth for the user, usually, we want to delay the search execution which you can do with debounceTime
.
this.searchFilter.pipe(debounceTime(500), distinctUntilChanged())
.subscribe(value => {
if (value && value.length >= 3) {
this.filterByName(value);
} else {
this.clearFilter();
}
});
To perform the search, I hide and show the nodes using a css class. This is done directly on the presentation collection which is flat and very easy to filter.
treeControl: FlatTreeControl<SkillFlatNode>;
this.treeControl.dataNodes
First, I hide all and then show only those that match the criteria. Finally, I want to show their parents, but this is specific for my tree structure.
private filterByName(term: string): void {
const filteredItems = this.treeControl.dataNodes.filter(
x => x.value.DisplayName.toLowerCase().indexOf(term.toLowerCase()) === -1
);
filteredItems.map(x => {
x.visible = false;
});
const visibleItems = this.treeControl.dataNodes.filter(
x => x.value.IsSkill &&
x.value.DisplayName.toLowerCase().indexOf(term.toLowerCase()) > -1
);
visibleItems.map( x => {
x.visible = true;
this.markParent(x);
});
}
Finally, here is the clear filter:
private clearFilter(): void {
this.treeControl.dataNodes.forEach(x => x.visible = true);
}
Don't make the same mistake like I did and try to filter the input collection (this.dataSource.data
in my case) because you will lose your selection or you will have to map it back to the presentation.
Here is my initial data:
this.treeFlattener = new MatTreeFlattener(
this.transformer, this._getLevel, this._isExpandable, this._getChildren
);
this.treeControl = new FlatTreeControl<SkillFlatNode>(
this._getLevel, this._isExpandable
);
this.dataSource = new MatTreeFlatDataSource(
this.treeControl, this.treeFlattener
);
skillService.dataChange.subscribe(data => {
this.dataSource.data = data;
});
I solved the problem by creating a new data source(filtered).
I will explain the example of the shared link: I filtered the data with filter(filterText: string)
in ChecklistDatabase
and triggered a dataChange
event. Then datasource.data
was changed by a handled event in TreeChecklistExample
. Thus the data source has been modified.
filter(filterText: string) {
let filteredTreeData;
if (filterText) {
filteredTreeData = this.treeData.filter(
//There is filter function in the sample
);
} else {
filteredTreeData = this.treeData;
}
// file node as children.
const data = this.buildFileTree(filteredTreeData, '0');
// Notify the change. !!!IMPORTANT
this.dataChange.next(data);
}
I am able to filter a tree by using simple recursion. Below are the code snippets:
The filter()
function is called on (keyup)
of input type="text"
. cloneDeep
function is imported from lodash import * as cloneDeep from 'lodash/cloneDeep';
this.searchString
is the string value for the filter text.
filter() {
const clonedTreeLocal = cloneDeep(this.clonedTree);
this.recursiveNodeEliminator(clonedTreeLocal);
this.dataSource.data = clonedTreeLocal;
this.treeControl.expandAll();
}
The tree structure is defined by the interface
export interface ITreeDataStructure {
Id?: number;
name: string;
type: string;
children?: Array<ITreeDataStructure>;
}
The actual filtering is done by the function recursiveNodeEliminator
recursiveNodeEliminator(tree: Array<ITreeDataStructure>): boolean {
for (let index = tree.length - 1; index >= 0; index--) {
const node = tree[index];
if (node.children) {
const parentCanBeEliminated = this.recursiveNodeEliminator(node.children);
if (parentCanBeEliminated) {
if (node.name.toLocaleLowerCase().indexOf(this.searchString.toLocaleLowerCase()) === -1) {
tree.splice(index, 1);
}
}
} else {
// Its a leaf node. No more branches.
if (node.name.toLocaleLowerCase().indexOf(this.searchString.toLocaleLowerCase()) === -1) {
tree.splice(index, 1);
}
}
}
return tree.length === 0;
}
Stackblitz link for mat-tree filter
If Anyone need to visually filter the mat tree without modifying the datasource, then go for this solution.
Basically the idea is to hide the nodes which are not part of the search string.
Input field
<input [(ngModel)]="searchString" />
call filter function for leaf node(this is done in the first mat-tree-node)
<mat-tree-node
*matTreeNodeDef="let node"
[style.display]="
filterLeafNode(node) ? 'none' : 'block'
"
.....
......
call filter function for the nodes other than leaf node(this is done in the second mat-tree-node)
<mat-tree-node
*matTreeNodeDef="let node; when: hasChild"
[style.display]="filterParentNode(node) ? 'none' : 'block'"
.....
.....
filterLeafNode function
filterLeafNode(node: TodoItemFlatNode): boolean {
if (!this.searchString) {
return false
}
return node.item.toLowerCase()
.indexOf(this.searchString?.toLowerCase()) === -1
}
filterParentNode function
filterParentNode(node: TodoItemFlatNode): boolean {
if (
!this.searchString ||
node.item.toLowerCase()
.indexOf(
this.searchString?.toLowerCase()
) !== -1
) {
return false
}
const descendants = this.treeControl.getDescendants(node)
if (
descendants.some(
(descendantNode) =>
descendantNode.item
.toLowerCase()
.indexOf(this.searchString?.toLowerCase()) !== -1
)
) {
return false
}
return true
}
This is for Model Filters. Create 2 lists, one to store all your data and one to be used as a datasource.
SearchCategory(searchText){
this.searchText=searchText;
this.categories=this.categoryNameSearch(this.AllCategories,searchText)
this.dataSource.data = this.categories;
}
categoryNameSearch(categorys:Category[],searchText):Category[]{
let category:Category[];
category=categorys.filter(f=>this.converter(f.name).includes(this.converter(searchText)))
categorys.forEach(element => {
this.categoryNameSearch(element.childrens,searchText).forEach(e=>category.push(e))
});
return category;
}
converter(text) {
var trMap = {
'çÇ':'c',
'ğĞ':'g',
'şŞ':'s',
'üÜ':'u',
'ıİ':'i',
'öÖ':'o'
};
for(var key in trMap) {
text = text.replace(new RegExp('['+key+']','g'), trMap[key]);
}
return text.replace(/[^-a-zA-Z0-9\s]+/ig, '') // remove non-alphanumeric chars
.replace(/\s/gi, "-") // convert spaces to dashes
.replace(/[-]+/gi, "-") // trim repeated dashes
.toLowerCase();
}
Html Side
<input type="text" (ngModelChange)="SearchCategory($event)" placeholder="Search Category" class="form-control"/>
First add an input as the filter in the view. Bind keyup event to rxjs Subject
<input type="text" matInput placeholder="search" #filter (keyup)="keyEvent.next($event)" [(ngModel)]="keyword">
Then query your backend to filter the tree node with keyword
this.keyEvent.pipe(
map((e: any) => e.target.value.toLowerCase()),
debounceTime(500),
distinctUntilChanged(),
switchMap((keyword: string) => {
if (keyword && keyword.length > 2) {
return this.yourservice.searchForData(this.entId, keyword);
} else {
return of();
}
})
)
.subscribe((r) => {
this.nestedDataSource.data = r;
this.nestedTreeControl.dataNodes = r;
this.nestedTreeControl.expandAll();
});