Skip to content

Commit 28196a3

Browse files
ellingemrchief
authored andcommitted
feat: Add support for custom search filtering on nodes (dowjones#270) ✨
New prop: `searchPredicate` - Adds support to filter nodes using custom search logic. For details, checkout README.
1 parent cbb8561 commit 28196a3

File tree

5 files changed

+66
-7
lines changed

5 files changed

+66
-7
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ A lightweight and fast control to render a select component that can display hie
6060
- [always](#always)
6161
- [form states (disabled|readOnly)](#formstates)
6262
- [id](#id)
63+
- [searchPredicate](#searchPredicate)
6364
- [Styling and Customization](#styling-and-customization)
6465
- [Using default styles](#default-styles)
6566
- [Customizing with Bootstrap, Material Design styles](#customizing-styles)
@@ -386,6 +387,20 @@ Specific id for container. The container renders with a default id of `rdtsN` wh
386387

387388
Use to ensure a own unique id when a simple counter is not sufficient, e.g in a partial server render (SSR)
388389

390+
### searchPredicate
391+
392+
Type: `function`
393+
394+
Optional search predicate to override the default case insensitive contains match on node labels. Example:
395+
396+
```jsx
397+
function searchPredicate(node, searchTerm) {
398+
return node.customData && node.customData.toLower().indexOf(searchTerm) >= 0
399+
}
400+
401+
return <DropdownTreeSelect data={data} searchPredicate={searchPredicate} />
402+
```
403+
389404
## Styling and Customization
390405

391406
### Default styles

src/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class DropdownTreeSelect extends Component {
4747
disabled: PropTypes.bool,
4848
readOnly: PropTypes.bool,
4949
id: PropTypes.string,
50+
searchPredicate: PropTypes.func,
5051
}
5152

5253
static defaultProps = {
@@ -66,12 +67,13 @@ class DropdownTreeSelect extends Component {
6667
this.clientId = props.id || clientIdGenerator.get(this)
6768
}
6869

69-
initNewProps = ({ data, mode, showDropdown, showPartiallySelected }) => {
70+
initNewProps = ({ data, mode, showDropdown, showPartiallySelected, searchPredicate }) => {
7071
this.treeManager = new TreeManager({
7172
data,
7273
mode,
7374
showPartiallySelected,
7475
rootPrefixId: this.clientId,
76+
searchPredicate,
7577
})
7678
// Restore focus-state
7779
const currentFocusNode = this.state.currentFocus && this.treeManager.getNodeById(this.state.currentFocus)

src/tree-manager/index.js

+16-6
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import nodeVisitor from './nodeVisitor'
55
import keyboardNavigation, { FocusActionNames } from './keyboardNavigation'
66

77
class TreeManager {
8-
constructor({ data, mode, showPartiallySelected, rootPrefixId }) {
8+
constructor({ data, mode, showPartiallySelected, rootPrefixId, searchPredicate }) {
99
this._src = data
1010
this.simpleSelect = mode === 'simpleSelect'
1111
this.radioSelect = mode === 'radioSelect'
1212
this.hierarchical = mode === 'hierarchical'
13+
this.searchPredicate = searchPredicate
1314
const { list, defaultValues, singleSelectedNode } = flattenTree({
1415
tree: JSON.parse(JSON.stringify(data)),
1516
simple: this.simpleSelect,
@@ -49,11 +50,7 @@ class TreeManager {
4950

5051
const matches = []
5152

52-
const addOnMatch = node => {
53-
if (node.label.toLowerCase().indexOf(searchTerm) >= 0) {
54-
matches.push(node._id)
55-
}
56-
}
53+
const addOnMatch = this._getAddOnMatch(matches, searchTerm)
5754

5855
if (closestMatch !== searchTerm) {
5956
const superMatches = this.searchMaps.get(closestMatch)
@@ -278,6 +275,19 @@ class TreeManager {
278275

279276
return keyboardNavigation.handleToggleNavigationkey(action, prevFocus, readOnly, onToggleChecked, onToggleExpanded)
280277
}
278+
279+
_getAddOnMatch(matches, searchTerm) {
280+
let isMatch = (node, term) => node.label.toLowerCase().indexOf(term) >= 0
281+
if (typeof this.searchPredicate === 'function') {
282+
isMatch = this.searchPredicate
283+
}
284+
285+
return node => {
286+
if (isMatch(node, searchTerm)) {
287+
matches.push(node._id)
288+
}
289+
}
290+
}
281291
}
282292

283293
export default TreeManager

src/tree-manager/tests/index.test.js

+30
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,36 @@ test('should get matching nodes with mixed case when searched', t => {
463463
t.is(matchTree.get('c2'), undefined)
464464
})
465465

466+
test('should get matching nodes when using custom search predicate', t => {
467+
const tree = {
468+
id: 'i1',
469+
label: 'search me',
470+
value: 'v1',
471+
children: [
472+
{
473+
id: 'c1',
474+
label: 'SeaRch me too',
475+
value: 'l1v1',
476+
children: [
477+
{
478+
id: 'c2',
479+
label: 'I have some extra data to filter on',
480+
customField: "I'm a little TeApOt",
481+
value: 'l2v1',
482+
},
483+
],
484+
},
485+
],
486+
}
487+
const searchPredicate = (node, term) => node.customField && node.customField.toLowerCase().indexOf(term) >= 0
488+
const manager = new TreeManager({ data: tree, searchPredicate })
489+
const { allNodesHidden, tree: matchTree } = manager.filterTree('tEaPoT')
490+
t.false(allNodesHidden)
491+
const nodes = ['i1', 'c1']
492+
nodes.forEach(n => t.is(matchTree.get(n), undefined))
493+
t.not(matchTree.get('c2'), undefined)
494+
})
495+
466496
test('should uncheck previous node in simple select mode', t => {
467497
const tree = [
468498
{

types/react-dropdown-tree-select.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ declare module 'react-dropdown-tree-select' {
9595
* Use to ensure a own unique id when a simple counter is not sufficient, e.g in a partial server render (SSR)
9696
*/
9797
id?: string
98+
/** Optional search predicate to override the default case insensitive contains match on node labels. */
99+
searchPredicate?: (currentNode: TreeNode, searchTerm: string) => boolean
98100
}
99101

100102
export interface DropdownTreeSelectState {

0 commit comments

Comments
 (0)