Filtering Hierarchical Data

The CollectionView class supports filtering only for items that are direct children of the collection. In most cases, it does not work well for hierarchical data. Filtering hierarchical data is not a trivial exercise because when a child element is visible, all its ancestors should also be visible. The grid below shows how you can implement a simple hierarchical binding method that will show cities that match the filter and states that match the filter or contain cities that do. For example, try typing 'San' in the filter box:

import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; import * as wjGrid from '@grapecity/wijmo.grid'; import { getData } from './data'; // document.readyState === 'complete' ? init() : window.onload = init; // function init() { // // create tree-style grid var theGrid = new wjGrid.FlexGrid('#theGrid', { childItemsPath: 'cities', headersVisibility: 'Column', itemsSource: getData(), }); // // update filter document.getElementById('filter').addEventListener('input', function (e) { var filter = e.target.value.toLowerCase(); applyHierarchicalFilter(theGrid, filter); }); // // update row visibility function applyHierarchicalFilter(grid, filter) { var rows = grid.rows; 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 = 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; } } } } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Filtering Hierarchical Data</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div class="container-fluid"> <div class="input-group"> <div class="input-group-addon"><span class="glyphicon glyphicon-search"></span></div> <input id="filter" class="form-control" placeholder="Filter"> </div> <div id="theGrid"> </div> <p> For more details in hierarchical filtering, please see our <a href="https://wijmo.com/blog/filter-hierarchical-data-flexgrid/" target="_blank">How to Filter Hierarchical Data in FlexGrid and Angular</a> blog.</p> </div> </body> </html> // some hierarchical data export function getData() { return [ { 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 } ] } ]; } .wj-flexgrid { max-height: 250px; margin: 10px 0; } .wj-cell.wj-group { border: none; } .wj-cell.wj-group:not(.wj-state-selected):not(.wj-state-multi-selected) { background-color: white; } body { margin-bottom: 20px; } import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; import { Component, enableProdMode, NgModule, ViewChild } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { BrowserModule } from '@angular/platform-browser'; import { WjGridModule } from '@grapecity/wijmo.angular2.grid'; import * as wjcGrid from '@grapecity/wijmo.grid'; @Component({ selector: 'app-component', templateUrl: 'src/app.component.html' }) export class AppComponent { data: any[]; constructor() { this.data = this._getData(); } @ViewChild('flex') flex: wjcGrid.FlexGrid; filter(e: any) { let filter = (<HTMLInputElement>e.target).value.toLowerCase(); this._applyHierarchicalFilter(this.flex, filter); } // update row visibility private _applyHierarchicalFilter(grid: wjcGrid.FlexGrid, filter: string) { let rows = grid.rows; for (let i = 0; i < rows.length; i++) { let 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 let stateVisible = state.name.toLowerCase().indexOf(filter) >= 0; if (stateVisible) { // it does, so show the state and all its cities for (let j = rng.topRow; j <= rng.bottomRow; j++) { rows[j].visible = true; } } else { // it does not, so check the cities for (let j = rng.topRow + 1; j <= rng.bottomRow; j++) { let city = rows[j].dataItem, cityVisible = city.name.toLowerCase().indexOf(filter) >= 0; rows[j].visible = cityVisible; stateVisible = 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; } } } private _getData() { return [ { 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 }] } ]; } } @NgModule({ imports: [WjGridModule, BrowserModule], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { } enableProdMode(); // Bootstrap application with hash style navigation and global services. platformBrowserDynamic().bootstrapModule(AppModule); <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Filtering Hierarchical Data</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Polyfills --> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.min.js"></script> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.js"></script> <script src="systemjs.config.js"></script> <script> // workaround to load 'rxjs/operators' from the rxjs bundle System.import('rxjs').then(function (m) { System.set(SystemJS.resolveSync('rxjs/operators'), System.newModule(m.operators)); System.import('./src/app.component'); }); </script> </head> <body> <app-component></app-component> </body> </html> <div class="container-fluid"> <div class="input-group"> <div class="input-group-addon"><span class="glyphicon glyphicon-search"></span></div> <input (input)="filter($event)" class="form-control" placeholder="Filter"> </div> <!-- the grid --> <wj-flex-grid #flex [childItemsPath]="'cities'" [headersVisibility]="'Column'" [(itemsSource)]="data"> </wj-flex-grid> <p> For more details in hierarchical filtering, please see our <a href="https://wijmo.com/blog/filter-hierarchical-data-flexgrid/" target="_blank">How to Filter Hierarchical Data in FlexGrid and Angular</a> blog. </p> </div> .wj-flexgrid { max-height: 250px; margin: 10px 0; } .wj-cell.wj-group { border: none; } .wj-cell.wj-group:not(.wj-state-selected):not(.wj-state-multi-selected) { background-color: white; } body { margin-bottom: 20px; } <template> <div class="container-fluid"> <div class="input-group"> <div class="input-group-addon"> <span class="glyphicon glyphicon-search"></span> </div> <input @input="filter($event)" class="form-control" placeholder="Filter"> </div> <!-- the grid --> <wj-flex-grid :childItemsPath="'cities'" :headersVisibility="'Column'" :itemsSource="data" :initialized="initialized" ></wj-flex-grid> <p> For more details in hierarchical filtering, please see our <a href="https://wijmo.com/blog/filter-hierarchical-data-flexgrid/" target="_blank">How to Filter Hierarchical Data in FlexGrid and Angular</a> blog. </p> </div> </template> <script> import "@grapecity/wijmo.styles/wijmo.css"; import "bootstrap.css"; import Vue from "vue"; import "@grapecity/wijmo.vue2.grid"; import * as wjcGrid from "@grapecity/wijmo.grid"; import "@grapecity/wijmo.vue2.grid.filter"; import * as wjcCore from "@grapecity/wijmo"; let App = Vue.extend({ name: "app", data: function() { return { data: this.getData() }; }, methods: { getData: function() { return [ { 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 } ] } ]; }, filter: function(e) { let filter = e.target.value.toLowerCase(); this.applyHierarchicalFilter(this.flex, filter); }, // update row visibility applyHierarchicalFilter: function(grid, filter) { let rows = grid.rows; for (let i = 0; i < rows.length; i++) { let 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 let stateVisible = state.name.toLowerCase().indexOf(filter) >= 0; if (stateVisible) { // it does, so show the state and all its cities for (let j = rng.topRow; j <= rng.bottomRow; j++) { rows[j].visible = true; } } else { // it does not, so check the cities for (let j = rng.topRow + 1; j <= rng.bottomRow; j++) { let city = rows[j].dataItem, cityVisible = city.name.toLowerCase().indexOf(filter) >= 0; rows[j].visible = cityVisible; stateVisible = 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; } } }, initialized: function(flex) { this.flex = flex; } } }); new Vue({ render: h => h(App) }).$mount("#app"); </script> <style> .wj-flexgrid { max-height: 250px; margin: 10px 0; } .wj-cell.wj-group { border: none; } .wj-cell.wj-group:not(.wj-state-selected):not(.wj-state-multi-selected) { background-color: white; } body { margin-bottom: 20px; } </style> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>AutoComplete</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app.vue'); </script> </head> <body> <div id="app"> </div> </body> </html> import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './app.css'; // import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as wjGrid from '@grapecity/wijmo.react.grid'; import { getData } from "./data"; class App extends React.Component { constructor(props) { super(props); this.state = { data: getData() }; } render() { return <div className="container-fluid"> <div className="input-group"> <div className="input-group-addon"> <span className="glyphicon glyphicon-search"></span> </div> <input onInput={this.filter.bind(this)} className="form-control" placeholder="Filter"/> </div> <wjGrid.FlexGrid childItemsPath="cities" headersVisibility="Column" initialized={this.initialized.bind(this)} itemsSource={this.state.data}> </wjGrid.FlexGrid> <p> For more details in hierarchical filtering, please see our{' '} <a href="https://wijmo.com/blog/filter-hierarchical-data-flexgrid/" target="_blank"> How to Filter Hierarchical Data in FlexGrid and Angular </a> {' '}blog. </p> </div>; } applyHierarchicalFilter(grid, filter) { let rows = grid.rows; for (let i = 0; i < rows.length; i++) { let 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 let stateVisible = state.name.toLowerCase().indexOf(filter) >= 0; if (stateVisible) { // it does, so show the state and all its cities for (let j = rng.topRow; j <= rng.bottomRow; j++) { rows[j].visible = true; } } else { // it does not, so check the cities for (let j = rng.topRow + 1; j <= rng.bottomRow; j++) { let city = rows[j].dataItem, cityVisible = city.name.toLowerCase().indexOf(filter) >= 0; rows[j].visible = cityVisible; stateVisible = 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; } } } filter(e) { let filter = e.target.value.toLowerCase(); this.applyHierarchicalFilter(this.flex, filter); } initialized(flex) { this.flex = flex; } } ReactDOM.render(<App />, document.getElementById('app')); <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Custom Filter Icons</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div id="app"></div> </body> </html> .wj-flexgrid { max-height: 250px; margin: 10px 0; } .wj-cell.wj-group { border: none; } .wj-cell.wj-group:not(.wj-state-selected):not(.wj-state-multi-selected) { background-color: white; } body { margin-bottom: 20px; } export function getData() { return [ { 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 } ] } ]; }