Tuesday, December 6, 2022

Filtering Angular Material NestedTreeControl Nodes

The Angular Material Tree, the MatTree, is the ideal control for displaying hierarchical data. Not only can it represent a variable number of levels, but each item in the tree can possess a different number of children and levels. In the 2nd installment of the Create a Nested Multi-select Tree with in Angular series, we utilized the MatTree to create an alternative to the HTML SELECT control that is better suited for working with hierarchical data. As part of that web development tutorial, we learned how to retrieve selected nodes using the NestedTreeControl’s getDescendants() method. As we will see in today’s article, that same method can help us filter nodes based on a number of conditions in order to limit visible nodes to those which are:

  • selected (i.e., checked)
  • match a given search criteria

To achieve both the above ends, we will refactor the Nested Multi-select Tree Demo from the aforementioned article by adding a Show Selected Items toggle control as well as a Filter textbox.

Hiding Unchecked Items with CSS display

Node filtering is best achieved using a combination of TypeScript code and template conditions rather than mutating the underlying data structure. To do that, we can use the CSS display property to either show or hide a given node. But, before we get to that, let’s add the toggle control in the template:

<p>
  <mat-slide-toggle class="toggle-show-only-selected" [(ngModel)]="showOnlySelected">Show only selected</mat-slide-toggle>
</p>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="example-tree">

Thanks to two-way binding, the toggle control’s state is immediately reflected in the showSelectedOnly variable’s value:

export class TreeNestedOverviewExample {
  public showOnlySelected = false;
  // ...

We can then set each node’s display property using Angular property binding.

For leaf nodes:

<mat-tree-node 
  *matTreeNodeDef="let node" 
  matTreeNodeToggle
  [style.display]="showOnlySelected && !node.selected ? 'none' : 'block'"
>

For root nodes:

<mat-tree-node 
  *matTreeNodeDef="let node; when: hasChild"
  matTreeNodeToggle
  [style.display]="showOnlySelected && !(node.selected || node.indeterminate) ? 'none' : 'block'"
>

Initializing the showOnlySelected variable to false puts the toggle slider to the off position and shows all nodes by default:

Angular MatTree Nodes

 

Sliding the toggle to the on position now hides all unselected (i.e. unchecked) nodes:

Wirking with Angular Nodes
Read: Create a Nested Multi-select Tree in Angular

 

Filtering Nodes By Search Criteria with TypeScript and CSS

You have to admit that the first part of this tutorial was pretty easy. This section is a little more substantial. The same approach of combining TypeScript code with template conditions will continue to serve us well. This time, however, some TypeScript logic will be necessary in order to determine whether a node should be hidden or visible.

With that in mind, let’s begin by adding the filter control to the template:

<p>
  <label>Filter by:
    <input [(ngModel)]="searchString" />
  </label>
  <mat-slide-toggle class="toggle-show-only-selected" [(ngModel)]="showOnlySelected">Show only selected</mat-slide-toggle>
</p>

This time around, we will call the hideLeafNode() and hideParentNode() methods to determine whether to show or hide the node based on the searchString.

For leaf nodes:

<mat-tree-node 
  *matTreeNodeDef="let node" 
  matTreeNodeToggle
  [style.display]="showOnlySelected && !node.selected || hideLeafNode(node) ? 'none' : 'block'"
>

I’ve often said that variables are far preferable to method calls in template expressions because the latter will be invoked numerous times whenever the user interacts with a node element. However, there are times when a method may be the better choice. In this case, the deciding factor is that hideLeafNode() and hideParentNode() will receive the node as an argument, allowing them to check its value against the searchString. In any event, there are ways to minimize the number of method calls, as we will see a little later on.

Here is the updated root node element markup:

<mat-tree-node 
  *matTreeNodeDef="let node; when: hasChild"
  matTreeNodeToggle
  [style.display]="showOnlySelected && !(node.selected || node.indeterminate) || hideParentNode(node) ? 'none' : 'block'"
>

There are many ways to check if a string contains a given substring; one way to perform a case insensitive test is to use a Regular Expression or RegEx. We are using the constructor so that we can use the searchString as the pattern:

public hideLeafNode(node: VehicleNode): boolean {
  return this.showOnlySelected && !node.selected 
    ? true 
    : new RegExp(this.searchString, 'i').test(node.name) === false;
}

Notice that we still have to take into account that the node may already be hidden by the Show-only selected filter. In that case, the method should return true without even looking for the searchString.

Since RegEx patterns can contain a myriad of special characters, it would probably be wise to restrict the allowable characters on the text input to those of node values – say letters, numbers, and spaces – were this a production application.

Hiding a parent node depends on whether or not all of its child nodes contain the searchString (i.e., are hidden). To do that, we will collect all of the node’s leaf children and run them through the hideLeafNode() method:

public hideParentNode(node: VehicleNode): boolean {
  return this.treeControl
      .getDescendants(node)
      .filter(node => node.children == null || node.children.length === 0)
      .every(node => this.hideLeafNode(node));
}

Here’s a screen capture of the search filter in action:

CSS and TypeScript Tree Nodes

Read: TypeScript Coding Outside of Angular Applications

 

Limiting hide*Node() Method Invocations

As mentioned earlier, relying on methods to resolve template conditions can be a real resource hog. For that reason, you should always aim to keep the number of method invocations to a bare minimum by combining comparison operators in such a way that the method is the last operand in the chain. In the case of the hideLeafNode() and hideParentNode() methods, they only need to be invoked when the filter text input – or the searchString – contains a value. With a little help from JavaScript falsiness, we can just add the searchString to the template conditions:

//child nodes
(showOnlySelected || this.searchString) && hideLeafNode(node) ? 'none' : 'block'"

//parent nodes
[style.display]="(showOnlySelected && !(node.selected || node.indeterminate)) || this.searchString && hideParentNode(node) ? 'none' : 'block'"

There’s a demo of today’s code on stackblitz.

Conclusion

Indeed, there are many ways for web developers to filter nodes of a MatTree. The two filtering types that we explored here today are but a couple of the most common. We also ignored any additional actions that may accompany a filtering operation. In the next article, we will be taking a look at two of these. Both related to search string filtering, we will expand nodes that contain the search string as well as highlight the matched substring in the node labels.

Read more Cascading Style Sheets (CSS) web development tutorials.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Popular Articles

Featured