Searching TreeViews

Searching TreeViews is not trivial because of their hierarchical nature. Nodes typically reflect a context defined by in part by their parent nodes but also by additional content associated with the node.

For example, if a user searched the TreeView below for "Electronics", you may or may not want to include the child nodes in the results. Furthermore, if items contained detailed descriptions, you might want to add keywords to help in the search. So if a user typed for example "beard", you would probably want the "Trimmers/Shavers" node to be selected.

The AutoComplete control provides a good way to implement a search box to be used with the TreeView. In this sample, we build a flat searchArray with the full node paths and keywords and use that as an itemsSource for searching through the TreeView.

In addition to the 'itemsSource' and 'displayMemberPath' properties, we use the 'searchMemberPath' property to specify the name of the field that contains the keywords to include in the search.

For example, try typing 'beard', 'collect', or 'food' in the search box:

import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; import * as wjInput from '@grapecity/wijmo.input'; import * as wjNav from '@grapecity/wijmo.nav'; import { getData } from './data'; document.readyState === 'complete' ? init() : window.onload = init; class searchItem { } function getSearchList(items, searchList, path) { // set defaults if (searchList == null) searchList = []; if (path == null) path = ''; // add items and sub-items for (var i = 0; i < items.length; i++) { var item = items[i]; searchList.push({ item: item, path: path + item.header, keywords: item.keywords }); if (item.items) { getSearchList(item.items, searchList, path + item.header + ' / '); } } return searchList; } function init() { // create the tree var tree = new wjNav.TreeView('#theTree', { itemsSource: getData(), displayMemberPath: 'header', childItemsPath: 'items', }); // create the search AutoComplete var search = new wjInput.AutoComplete('#search', { itemsSource: getSearchList(tree.itemsSource), selectedIndex: -1, displayMemberPath: 'path', searchMemberPath: 'keywords', selectedIndexChanged: function (s) { if (s.selectedItem) { tree.selectedItem = s.selectedItem.item; } } }); } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo TreeView Searching</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"> <label for="search">Search: </label> <div id="search"></div> <div id="theTree"></div> </div> </body> </html> export function getData() { return [ { header: 'Electronics', items: [ { header: 'Trimmers/Shavers', keywords: 'beard hair' }, { header: 'Tablets', keywords: 'screen computer android ios facebook' }, { header: 'Phones', keywords: 'talk listen email fabebook', items: [ { header: 'Apple' }, { header: 'Motorola' }, { header: 'Nokia' }, { header: 'Samsung' } ] }, { header: 'Speakers', keywords: 'music loudspeaker' }, { header: 'Monitors', keywords: 'screen color lcd oled' } ] }, { header: 'Toys', items: [ { header: 'Shopkins', keywords: 'animals collectibles' }, { header: 'Train Sets', keywords: 'models rail collectibles' }, { header: 'Science Kit', keywords: 'education physics chemistry' }, { header: 'Play-Doh', keywords: 'clay sculpt models' }, { header: 'Crayola', keywords: 'drawing painting wax chalk pencils' } ] }, { header: 'Home', items: [ { header: 'Coffee Maker', keywords: 'kitchen appliance drink' }, { header: 'Breadmaker', keywords: 'kitchen appliance food cooking' }, { header: 'Solar Panel', keywords: 'electric sun renewable energy' }, { header: 'Work Table', keywords: 'shop tools' }, { header: 'Propane Grill', keywords: 'food cooking barbecue meat' } ] } ]; } .wj-control { margin-bottom: 6px; } .wj-treeview { display:block; font-size: 120%; margin-bottom: 8px; padding: 6px; background: #f0f0f0; box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); } body { margin-bottom: 24pt; } import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; import { Component, Inject, enableProdMode, NgModule, ViewChild, AfterViewInit } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { BrowserModule } from '@angular/platform-browser'; import { WjInputModule, WjAutoComplete } from '@grapecity/wijmo.angular2.input'; import { WjNavModule, WjTreeView } from '@grapecity/wijmo.angular2.nav'; import { DataService, TreeItem } from './app.data'; class searchItem { item: any; path: string; keywords: string } @Component({ selector: 'app-component', templateUrl: 'src/app.component.html' }) export class AppComponent implements AfterViewInit { @ViewChild('theTree') theTree: WjTreeView; treeData: TreeItem[]; autoCompleteData: searchItem[]; constructor(@Inject(DataService) private dataService: DataService) { } ngAfterViewInit() { this.theTree.itemsSource = this.dataService.getData(); this.autoCompleteData = this._getSearchList(this.theTree.itemsSource); } onSelectedIndexChanged(s: WjAutoComplete) { if (s.selectedItem) { this.theTree.selectedItem = s.selectedItem.item; } } private _getSearchList(items: TreeItem[], searchList?: null | searchItem[], path?: string | null): searchItem[] { // set defaults if (searchList == null) searchList = []; if (path == null) path = ''; // add items and sub-items for (var i = 0; i < items.length; i++) { var item = items[i]; searchList.push({ item: item, path: path + item.header, keywords: item.keywords }); if (item.items) { this._getSearchList(item.items, searchList, path + item.header + ' / '); } } return searchList; } } @NgModule({ imports: [WjNavModule, WjInputModule, BrowserModule], declarations: [AppComponent], providers: [DataService], 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 TreeView Searching</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"> <label for="search">Search: </label> <wj-auto-complete #searchAutoComplete [itemsSource]="autoCompleteData" [selectedIndex]="-1" [displayMemberPath]="'path'" [searchMemberPath]="'keywords'" (selectedIndexChanged)="onSelectedIndexChanged(searchAutoComplete)"> </wj-auto-complete> <wj-tree-view #theTree [displayMemberPath]="'header'" [childItemsPath]="'items'"></wj-tree-view> </div> import { Injectable } from '@angular/core'; export class TreeItem { header: string; keywords?: string; items?: TreeItem[] } @Injectable() export class DataService { getData(): TreeItem[] { return [ { header: 'Electronics', items: [ { header: 'Trimmers/Shavers', keywords: 'beard hair' }, { header: 'Tablets', keywords: 'screen computer android ios facebook' }, { header: 'Phones', keywords: 'talk listen email fabebook', items: [ { header: 'Apple' }, { header: 'Motorola' }, { header: 'Nokia' }, { header: 'Samsung' } ] }, { header: 'Speakers', keywords: 'music loudspeaker' }, { header: 'Monitors', keywords: 'screen color lcd oled' } ] }, { header: 'Toys', items: [ { header: 'Shopkins', keywords: 'animals collectibles' }, { header: 'Train Sets', keywords: 'models rail collectibles' }, { header: 'Science Kit', keywords: 'education physics chemistry' }, { header: 'Play-Doh', keywords: 'clay sculpt models' }, { header: 'Crayola', keywords: 'drawing painting wax chalk pencils' } ] }, { header: 'Home', items: [ { header: 'Coffee Maker', keywords: 'kitchen appliance drink' }, { header: 'Breadmaker', keywords: 'kitchen appliance food cooking' }, { header: 'Solar Panel', keywords: 'electric sun renewable energy' }, { header: 'Work Table', keywords: 'shop tools' }, { header: 'Propane Grill', keywords: 'food cooking barbecue meat' } ] } ]; } } .wj-control { margin-bottom: 6px; } .wj-treeview { display:block; font-size: 120%; margin-bottom: 8px; padding: 6px; background: #f0f0f0; box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); } body { margin-bottom: 24pt; } <template> <div class="container-fluid"> <label for="search">Search: </label> <wj-auto-complete :items-source="autoCompleteData" :selected-index=-1 display-member-path="path" search-member-path="keywords" :selected-index-changed="onSelectedIndexChanged"> </wj-auto-complete> <wj-tree-view :items-source="data" display-member-path="header" child-items-path="items" :initialized="initTreeView"> </wj-tree-view> </div> </template> <script> import 'bootstrap.css'; import "@grapecity/wijmo.styles/wijmo.css"; import Vue from 'vue'; import '@grapecity/wijmo.vue2.input'; import '@grapecity/wijmo.vue2.nav'; import * as wjCore from '@grapecity/wijmo'; import { getData } from './data'; var wjTreeViewControl = null; var treeViewData= getData(); new Vue({ el: '#app', data: function () { return { data: treeViewData, autoCompleteData: _getSearchList(treeViewData) } }, methods:{ initTreeView: function(ctl){ wjTreeViewControl = ctl; }, onSelectedIndexChanged: function(s) { if (s.selectedItem) { wjTreeViewControl.selectedItem = s.selectedItem.item; } } } }) function _getSearchList(items, searchList, path) { // set defaults if (searchList == null) searchList = []; if (path == null) path = ''; // add items and sub-items for (var i = 0; i < items.length; i++) { var item = items[i]; searchList.push({ item: item, path: path + item.header, keywords: item.keywords }); if (item.items) { _getSearchList(item.items, searchList, path + item.header + ' / '); } } return searchList; } </script> <style> .container-fluid .wj-control { margin-bottom: 6px; } .container-fluid .wj-treeview { display:block; font-size: 120%; margin-bottom: 8px; padding: 6px; background: #f0f0f0; box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); } body { margin-bottom: 24pt; } </style> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo TreeView Searching</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> export function getData(){ return [ { header: 'Electronics', items: [ { header: 'Trimmers/Shavers', keywords: 'beard hair' }, { header: 'Tablets', keywords: 'screen computer android ios facebook' }, { header: 'Phones', keywords: 'talk listen email fabebook', items: [ { header: 'Apple' }, { header: 'Motorola' }, { header: 'Nokia' }, { header: 'Samsung' } ] }, { header: 'Speakers', keywords: 'music loudspeaker' }, { header: 'Monitors', keywords: 'screen color lcd oled' } ] }, { header: 'Toys', items: [ { header: 'Shopkins', keywords: 'animals collectibles' }, { header: 'Train Sets', keywords: 'models rail collectibles' }, { header: 'Science Kit', keywords: 'education physics chemistry' }, { header: 'Play-Doh', keywords: 'clay sculpt models' }, { header: 'Crayola', keywords: 'drawing painting wax chalk pencils' } ] }, { header: 'Home', items: [ { header: 'Coffee Maker', keywords: 'kitchen appliance drink' }, { header: 'Breadmaker', keywords: 'kitchen appliance food cooking' }, { header: 'Solar Panel', keywords: 'electric sun renewable energy' }, { header: 'Work Table', keywords: 'shop tools' }, { header: 'Propane Grill', keywords: 'food cooking barbecue meat' } ] } ]; } import './app.css'; import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; // import * as React from 'react'; import * as ReactDOM from 'react-dom'; // import * as wjNav from '@grapecity/wijmo.react.nav'; import * as wjInput from '@grapecity/wijmo.react.input'; import { getData } from './data'; class App extends React.Component { constructor(props) { super(props); this._wjTreeViewControl = null; this._treeViewData = getData(); this.state = { data: this._treeViewData, autoCompleteData: this.getSearchList(this._treeViewData, null, null) }; } render() { return (<div className="container-fluid"> <label htmlFor="search">Search: </label> <wjInput.AutoComplete itemsSource={this.state.autoCompleteData} selectedIndex={-1} displayMemberPath="path" searchMemberPath="keywords" selectedIndexChanged={this.onSelectedIndexChanged.bind(this)}> </wjInput.AutoComplete> <wjNav.TreeView itemsSource={this.state.data} displayMemberPath="header" childItemsPath="items" initialized={this.initTreeView.bind(this)}> </wjNav.TreeView> </div>); } initTreeView(ctl) { this._wjTreeViewControl = ctl; } onSelectedIndexChanged(s) { if (s.selectedItem) { this._wjTreeViewControl.selectedItem = s.selectedItem.item; } } getSearchList(items, searchList, path) { // set defaults if (searchList == null) searchList = []; if (path == null) path = ''; // add items and sub-items for (var i = 0; i < items.length; i++) { var item = items[i]; searchList.push({ item: item, path: path + item.header, keywords: item.keywords }); if (item.items) { this.getSearchList(item.items, searchList, path + item.header + ' / '); } } return searchList; } } 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>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'); </script> </head> <body> <div id="app"></div> </body> </html> .container-fluid .wj-control { margin-bottom: 6px; } .container-fluid .wj-treeview { display: block; font-size: 120%; margin-bottom: 8px; padding: 6px; background: #f0f0f0; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); } body { margin-bottom: 24pt; } export function getData() { return [ { header: 'Electronics', items: [ { header: 'Trimmers/Shavers', keywords: 'beard hair' }, { header: 'Tablets', keywords: 'screen computer android ios facebook' }, { header: 'Phones', keywords: 'talk listen email fabebook', items: [ { header: 'Apple' }, { header: 'Motorola' }, { header: 'Nokia' }, { header: 'Samsung' } ] }, { header: 'Speakers', keywords: 'music loudspeaker' }, { header: 'Monitors', keywords: 'screen color lcd oled' } ] }, { header: 'Toys', items: [ { header: 'Shopkins', keywords: 'animals collectibles' }, { header: 'Train Sets', keywords: 'models rail collectibles' }, { header: 'Science Kit', keywords: 'education physics chemistry' }, { header: 'Play-Doh', keywords: 'clay sculpt models' }, { header: 'Crayola', keywords: 'drawing painting wax chalk pencils' } ] }, { header: 'Home', items: [ { header: 'Coffee Maker', keywords: 'kitchen appliance drink' }, { header: 'Breadmaker', keywords: 'kitchen appliance food cooking' }, { header: 'Solar Panel', keywords: 'electric sun renewable energy' }, { header: 'Work Table', keywords: 'shop tools' }, { header: 'Propane Grill', keywords: 'food cooking barbecue meat' } ] } ]; }