Skip to content

Instantly share code, notes, and snippets.

@TimothyRHuertas
Last active August 24, 2019 08:28
Show Gist options
  • Save TimothyRHuertas/a85325a1e6216c7f0c05 to your computer and use it in GitHub Desktop.
Save TimothyRHuertas/a85325a1e6216c7f0c05 to your computer and use it in GitHub Desktop.
Filter react-treebeard

Alex Curtis created react-treebeard a data driven tree view for React. I needed to enable users to filter the tree's nodes based on input text. To accomplish this I transform the tree's data using 2 functions when the filter changes and rerender the view:

  1. filterTree Given a tree return a new tree keeping only the nodes that meet one of the following conditions:
    1. Has matching text
    2. Has a parent with matching text
    3. Has a child with matching text
  2. expandNodesWithMatchingDescendants - Given a tree mark all the nodes with matching descendants as toggled

Here are the aformentioned functions.

var defaultMatcher = (filterText, node) => {
    return node.name.toLowerCase().indexOf(filterText.toLowerCase()) !== -1
} 

var nodeMatchesOrHasMatchingDescendants = (node, filter, matcher) => {
    return matcher(filter, node) || //i match 
        (node.children && //or i have decendents and one of them match
        node.children.length &&
        !!node.children.find(childNode => nodeMatchesOrHasMatchingDescendants(childNode, filter, matcher)))
}

var filterTree = (node, filter, matcher=defaultMatcher) => {
    if(matcher(filter, node)){ //if im an exact match then all my children get to stay
        return node
    }
    else { //if not then only keep the ones that match or have matching descendants
        var filteredChildren
        
        if(node.children) {
            filteredChildren = node.children.filter(child => nodeMatchesOrHasMatchingDescendants(child, filter, matcher))
        }
        
        if(filteredChildren && filteredChildren.length){ 
            filteredChildren = filteredChildren.map(child => filterTree(child, filter, matcher))  
        }
        
        return Object.assign({}, node, {
            children: filteredChildren
        });      
    }
}

var expandNodesWithMatchingDescendants = (node, filter, matcher=defaultMatcher) => {
    var children = node.children 
    var shouldExpand = false
    
    if(children && children.length){
        var childrenWithMatches = node.children.filter(child => nodeMatchesOrHasMatchingDescendants(child, filter, matcher))
        shouldExpand = !!childrenWithMatches.length //I expand if any of my kids match
        
        if(shouldExpand) {//if im going to expand
            //go through all the matches and see if thier children need to expand
            children = childrenWithMatches.map(child => expandNodesWithMatchingDescendants(child, filter, matcher))    
        }    
    }  
    
    return Object.assign({}, node, {children: children, toggled: shouldExpand}) 
}

Put the beard and filter together by rendering the input box and the tree. Filter the data as the user types (onKeyUp).

    render() {
        return (
            <div>
                <input onKeyUp={this.handleFilterMouseUp.bind(this)} />
                <Treebeard data={this.state.data} />
            </div>
        )
    }
    handleFilterMouseUp(e){
        const filter = e.target.value.trim()

        if(filter){
            var data = filterTree(unfilteredTreeData, filter)
            data = expandNodesWithMatchingDescendants(data, filter)
            
            this.setState({data})    
        }
        else {
            this.setState({data: unfilteredTreeData})     
        }
    }

I modded the example that ships with react-treebeard. The changes can be seen in my fork.

@hdchinh
Copy link

hdchinh commented Aug 24, 2019

Thank you, it helps me save a lot of time. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment