How to filter a mat-tree component Angular Materia

2019-02-10 13:55发布

问题:

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?

回答1:

I solved the problem by creating new datasource(filtered).

stackblitz sample

If I will explain the example of sharing the link, I filtered data by filter(filterText: string) in ChecklistDatabase and trigger dataChange event and then datasource.data changed by handled event in TreeChecklistExample. Thus datasource 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);
}


回答2:

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();
});


回答3:

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 have 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 these 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 as me and try to filter the input collection (this.dataSource.data in my case) because you will lose you selection or you 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;
    });


回答4:

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;
  }