Filtering is an important feature for large data sets and may vary depending on the structure of the data being filtered. In this post, we'll walk through filtering hierarchical data in the FlexGrid control in an Angular application.

Get the Fiddle | Download Wijmo


FlexGrid provides support for filtering and allows you to define your own filter function as well. The most common way to do this through the ICollectionView interface, but ICollectionView was designed to handle only flat data, so to handle hierarchical data we'llneed a different approach.

The Problem


Imagine we have a hierarchical collection consisting of “state” objects that have a collection of “city” objects. This could be expressed in JavaScript as follows:

// some hierarchical data
var data = [
{
name: 'Washington', type: 'state', population: 6971, cities: [
{ name: 'Seattle', type: 'city', population: 652 },
{ name: 'Spokane', type: 'city', population: 210 }
]
}, {
name: 'Oregon', type: 'state', population: 3930, cities: [
{ name: 'Portland', type: 'city', population: 609 },
{ name: 'Eugene', type: 'city', population: 159 }
]
}, {
name: 'California', type: 'state', population: 38330, cities: [
{ name: 'Los Angeles', type: 'city', population: 3884 },
{ name: 'San Diego', type: 'city', population: 1356 },
{ name: 'San Francisco', type: 'city', population: 837 }
]
}
];


If you assigned this collection to a FlexGrid’s itemsSource property, the grid would automatically display the states, but not the cities.

If you then assigned the value “cities” to the grid’s childItemsPath property, the grid would show the states and then would add child items for each element in the “cities” array. Note that in this case we have only two levels, but this process could continue to any depth.

So far, so good. But the topic of discussion here is filtering this hierarchical data.

By default, the ICollectionView used internally by the FlexGrid only deals with flat data. So you could use it to filter the states only. But what if you wanted to filter by state AND city?

The Solution: Define Rules for the Filter


To do this, we start by defining a few rules for the filter:

  1. The filter will be applied to the “name” property only.

  2. If a state (top level item) satisfies the filter, all its cities will be displayed

  3. If a city (second level item) satisfies the filter, its parent state will be displayed.


Rule #1 is arbitrary. We could use more properties in the filter.

Rule #2 is also arbitrary. If you think showing states without their cities would be useful to users, the rule can be removed.

Rule #3 is the most important one. If a user searched for “Spokane”, the grid should display the city and also its parent state. Failing to do so would create a grid with orphan items.

We'll apply these rules directly to the grid, by traversing all rows and setting their visible property to true only if they pass the filter. The code looks like this:

// update row visibility
function updateRowVisibility(flex, filter) {
var rows = flex.rows,
filter = filter.toLowerCase();
for (var i = 0; i < rows.length; i++) {
var row = rows[i],
state = row.dataItem,
rng = row.getCellRange();

// handle states (level 0)
if (row.level == 0) {

// check if the state name matches the filter
var stateVisible = state.name.toLowerCase().indexOf(filter) >= 0;
if (stateVisible) {

// it does, so show the state and all its cities
for (var j = rng.topRow; j <= rng.bottomRow; j++) {
rows[j].visible = true;
}
} else {

// it does not, so check the cities
for (var j = rng.topRow + 1; j <= rng.bottomRow; j++) {
var city = rows[j].dataItem,
cityVisible = city.name.toLowerCase().indexOf(filter) >= 0;
rows[j].visible = cityVisible;
stateVisible |= cityVisible;
}

// if at least one city is visible, the state is visible
rows[i].visible = stateVisible;
}

// move on to the next group
i = rng.bottomRow;
}
}
}


The code uses the GroupRow’s getCellRange method to retrieve an object that contains the group’s top and bottom rows, then uses those limits to loop through entire states or individual cities.

Implementing the Solution in Angular


Let's get started.

  1. Create an index.html file.

  2. Add the references to the controls.

  3. Add the Angular Interop.js files.

  4. Add a layout for the template. You can copy and paste the code below:


<html>
<head>
<!-- Stylesheet -->
<link rel="stylesheet" type="text/css" href="http://cdn.wijmo.com/5.latest/styles/wijmo.min.css" />

<!-- Wijmo References -->
<script src="http://cdn.wijmo.com/5.latest/controls/wijmo.min.js"></script>
<script src="http://cdn.wijmo.com/5.latest/controls/wijmo.grid.min.js"></script>

<!-- Wijmo Angular Interop -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<script src="http://cdn.wijmo.com/5.latest/interop/angular/wijmo.angular.min.js"></script>

<!-- Angular module -->
<script src="app.js" type="text/javascript"></script>
</head>

<body>
<div ng-app="app" ng-controller="appCtrl">
<wj-flex-grid
control="flex"
style="height:260px;width:320px"
items-source="data"
headers-visibility="Column">
</wj-flex-grid>

</div>
</body>
</html>


Since we're building an Angular application, we need to create our Angular Module and Controller.

Create a new file called app.js and copy and paste the code below.  Note: the index.html markup already includes a script tag to reference this file. Be sure the file is in the right location on your machine.

'use strict';

// define app, include Wijmo 5 directives
var app = angular.module('app', ['wj']);

// controller
app.controller('appCtrl', function ($scope) {

});


We can now add our small data set to the controller:

 // some hierarchical data
var data = [
{
name: 'Washington', type: 'state', population: 6971, cities: [
{ name: 'Seattle', type: 'city', population: 652 },
{ name: 'Spokane', type: 'city', population: 210 }
]
},
{
name: 'Oregon', type: 'state', population: 3930, cities: [
{ name: 'Portland', type: 'city', population: 609 },
{ name: 'Eugene', type: 'city', population: 159 }
]
},
{
name: 'California', type: 'state', population: 38330, cities: [
{ name: 'Los Angeles', type: 'city', population: 3884 },
{ name: 'San Diego', type: 'city', population: 1356 },
{ name: 'San Francisco', type: 'city', population: 837 }
]
}
];
$scope.data = new wijmo.collections.CollectionView(data);


On the last line of code, we set the $scope.data object to a CollectionView object, passing the data variable as a parameter. As previously mentioned, the ICollectionView interface represents our data as a flat structure, like an array or list of Objects. If you open the index.html file in a browser, you'll only see the states of each item in the CollectionView.

CollectionView flat structure


In order to display the cities, we need to assign the childItemsPath property of the FlexGrid to "cities." In your index.html file, add the childItemsPath attribute to the wj-flex-grid element and set the attribute to cities like below:

<wj-flex-grid
control="flex"
style="height:260px;width:320px"
items-source="data"
child-items-path="cities"
headers-visibility="Column">
</wj-flex-grid>


If you save the file and refresh the browser, you'll see the city rows under each state. This is called a Tree View.

Filtering in a TreeView


With the Tree View enabled, it's now time to add the filter! If we use the CollectionView filter, only the parent item’s data properties will be considered when filtering or sorting. Instead we'll create our own filter function. We've already defined our filter function above, but there are a few more steps to finalize our application.

  1. In the index.html file, add an input element and the ng-model directive to allow Angular to detect changes in the input element:
     <p>
    <input ng-model="filter" />
    </p>

  2. In the app.js file, add a $watch function to listen for changes in the filter model.
    // update row visibility when filter changes
    $scope.$watch('filter', function() {
    updateRowVisibility();
    });

  3. Finally, add the function that filters! We'll handle filtering by simply hiding the rows that don’t match the filter. So as the user is typing, the grid will automatically update based on our filter function. Copy the updateRowVisibility function from above to the Angular controller.


Now that you've successfully implemented Hierarchical filtering, try using a more complicated data set or modifying the rules/behavior of your filter function.

Check out full implementation here:http://jsfiddle.net/troy_taylor/etx8em7w/