Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS filter not expanding filtered nodes

I'm using https://github.com/tchatel/angular-treeRepeat and am attempting to filter nodes that have not expanded. So I've modified this code to include AngularJS filter :

treeRepeat.html :

<p id="expand-collapse-all">
    <a href="" ng-click="expandAll()">Expand all</a>
    <a href="" ng-click="collapseAll()">Collapse all</a>
</p>


Filter : <input ng-model="myFilter" type="text">

<ul frang-tree>
  <li frang-tree-repeat="node in treeData | filter:myFilter">
      <div><span class="icon"
                 ng-class="{collapsed: node.collapsed, expanded: !node.collapsed}"
                 ng-show="node.children && node.children.length > 0"
                 ng-click="node.collapsed = !node.collapsed"></span>
           <span class="label"
                 ng-class="{folder: node.children && node.children.length > 0}"
                 ng-bind="node.label"
                 ng-click="action(node)"></span>
      </div>
      <ul ng-if="!node.collapsed && node.children && node.children.length > 0"
          frang-tree-insert-children="node.children  | filter:myFilter"></ul>
  </li>
</ul>

This works as expected if all of the tree nodes are expanded : Line 20 on controllers.js :

$scope.treeData = JSON.parse("[ { \"label\": \"root\", \"children\": [ { \"label\": \"folder A\", \"collapsed\": true, \"children\": [ { \"label\": \"folder B\", \"collapsed\": true, \"children\": [ { \"label\": \"file B1\", \"collapsed\": true }, { \"label\": \"file B2\", \"collapsed\": true } ] }, { \"label\": \"file A1\", \"collapsed\": true }, { \"label\": \"file A2\", \"collapsed\": true }, { \"label\": \"file A3\", \"collapsed\": true }, { \"label\": \"file A4\", \"collapsed\": true } ] }, { \"label\": \"folder C\", \"collapsed\": true, \"children\": [ { \"label\": \"folder D\", \"collapsed\": true, \"children\": [ { \"label\": \"folder E\", \"collapsed\": true, \"children\": [ { \"label\": \"file E1\", \"collapsed\": true }, { \"label\": \"file E2\", \"collapsed\": true }, { \"label\": \"file E3\", \"collapsed\": true } ] } ] }, { \"label\": \"folder F\", \"collapsed\": true, \"children\": [ { \"label\": \"file F1\", \"collapsed\": true }, { \"label\": \"file F2\", \"collapsed\": true } ] }, { \"label\": \"file C1\", \"collapsed\": true } ] }, { \"label\": \"folder G\", \"collapsed\": true, \"children\": [ { \"label\": \"file G1\", \"collapsed\": true }, { \"label\": \"file G2\", \"collapsed\": true }, { \"label\": \"file G3\", \"collapsed\": true }, { \"label\": \"file G4\", \"collapsed\": true } ] }, { \"label\": \"folder H\", \"collapsed\": true, \"children\": [ { \"label\": \"file H1\", \"collapsed\": true }, { \"label\": \"file H2\", \"collapsed\": true }, { \"label\": \"file H3\", \"collapsed\": true } ] } ] } ]");

But if the nodes are collapsed then the matched nodes expanded / viewable. The tree remains collapsed. Config for collapsed nodes : Line 21 on controllers.js :

$scope.treeData = JSON.parse("[ { \"label\": \"root\", \"children\": [ { \"label\": \"folder A\", \"collapsed\": true, \"children\": [ { \"label\": \"folder B\", \"collapsed\": true, \"children\": [ { \"label\": \"file B1\", \"collapsed\": true }, { \"label\": \"file B2\", \"collapsed\": true } ] }, { \"label\": \"file A1\", \"collapsed\": true }, { \"label\": \"file A2\", \"collapsed\": true }, { \"label\": \"file A3\", \"collapsed\": true }, { \"label\": \"file A4\", \"collapsed\": true } ] }, { \"label\": \"folder C\", \"collapsed\": true, \"children\": [ { \"label\": \"folder D\", \"collapsed\": true, \"children\": [ { \"label\": \"folder E\", \"collapsed\": true, \"children\": [ { \"label\": \"file E1\", \"collapsed\": true }, { \"label\": \"file E2\", \"collapsed\": true }, { \"label\": \"file E3\", \"collapsed\": true } ] } ] }, { \"label\": \"folder F\", \"collapsed\": true, \"children\": [ { \"label\": \"file F1\", \"collapsed\": true }, { \"label\": \"file F2\", \"collapsed\": true } ] }, { \"label\": \"file C1\", \"collapsed\": true } ] }, { \"label\": \"folder G\", \"collapsed\": true, \"children\": [ { \"label\": \"file G1\", \"collapsed\": true }, { \"label\": \"file G2\", \"collapsed\": true }, { \"label\": \"file G3\", \"collapsed\": true }, { \"label\": \"file G4\", \"collapsed\": true } ] }, { \"label\": \"folder H\", \"collapsed\": true, \"children\": [ { \"label\": \"file H1\", \"collapsed\": true }, { \"label\": \"file H2\", \"collapsed\": true }, { \"label\": \"file H3\", \"collapsed\": true } ] } ] } ]");

Plunkr : https://plnkr.co/edit/CtXlRfdreolTTc018c0A?p=preview

Do I need to manually expand the nodes as the user types or is there an angular config I can use to expand these nodes ?

I've tried adding a custom function that fires everytime user types :

  function matchChildNode(objData , parentNode) {

        angular.forEach(objData, function(childNode, key) {

                var searchText = "";
                //AngularJS does not initialise the searchText var until used. As the function is re-initialised for every node
                //need to check if is undefined
                if ($scope.searchText == undefined) {
                    searchText = ""
                } else {
                    searchText = $scope.searchText
                }

                if (searchText.toLowerCase() === childNode.label.toLowerCase()) {
                    parentNode.collapsed = false
                }       

                matchChildNode(childNode.children , childNode);

  });
  } 
}

But this is very inefficient as it traverses entire tree structure for each keyword user types. This also just works for exactly matched text : searchText.toLowerCase() === childNode.label.toLowerCase() . Have tried using contains instead of === with no success.

plnkr src :

app.css (removed due to stackoverflow 30000 character limitation when asking questions) 

directives.js (removed due to stackoverflow 30000 character limitation when asking questions) 

filter.js : 

'use strict';

angular.module('app.filters', []);

index.html : 


<!doctype html>
<html lang="en" ng-app="app">
<head>
  <meta charset="utf-8">
  <title>treeRepeat demo</title>
  <link rel="stylesheet" href="app.css"/>
</head>

<body>

  <h1>treeRepeat</h1>
  <div id="menu" ng-controller="MenuCtrl">
      <ul>
          <li ng-repeat="item in menu" ng-class="{selected: item == getCurrentMenuItem()}"><a href="#/{{item.index}}">{{item.shortLabel}}</a></li>
      </ul>
      <h2>{{getCurrentMenuItem().fullLabel}}</h2>
  </div>

  <ng-view></ng-view>

  <script src="angular.js"></script>
  <script src="angular-route.js"></script>
  <script src="app.js"></script>
  <script src="services.js"></script>
  <script src="controllers.js"></script>
  <script src="filters.js"></script>
  <script src="directives.js"></script>
</body>
</html>

services.js : 

'use strict';

angular.module('app.services', [])
    .constant('menu', []);

treerepeat.html : 

<p id="expand-collapse-all">
    <a href="" ng-click="expandAll()">Expand all</a>
    <a href="" ng-click="collapseAll()">Collapse all</a>
</p>


Filter : <input ng-model="myFilter" type="text">

<ul frang-tree>
  <li frang-tree-repeat="node in treeData | filter:myFilter">
      <div><span class="icon"
                 ng-class="{collapsed: node.collapsed, expanded: !node.collapsed}"
                 ng-show="node.children && node.children.length > 0"
                 ng-click="node.collapsed = !node.collapsed"></span>
           <span class="label"
                 ng-class="{folder: node.children && node.children.length > 0}"
                 ng-bind="node.label"
                 ng-click="action(node)"></span>
      </div>
      <ul ng-if="!node.collapsed && node.children && node.children.length > 0"
          frang-tree-insert-children="node.children  | filter:myFilter"></ul>
  </li>
</ul>
like image 624
blue-sky Avatar asked Jan 04 '16 15:01

blue-sky


1 Answers

You can achieve this by using a filter with a predicate function.

function(value, index): A predicate function can be used to write arbitrary filters. The function is called for each element of array. The final result is an array of those elements that the predicate returned true for.

  1. Create a predicate function

$scope.getFilter = function(value, index){
  if(!$scope.myFilter || $scope.myFilter === '' && (value.label === 'root')){
      $scope.collapseAll();
      return true;
  }
  if (findMatch(value, $scope.myFilter)){
      value.collapsed = false;
      return true;
    }else{
      value.collapsed = true;
      return false;
    }
}
  1. Create a function that checks if the current item is a match(this function is called from the predicate function)

function findMatch(value, filter) {
    var found = false;
    if(value.label.toLowerCase().indexOf(filter) > -1){
        value.collapsed = false;
        found = true;
    }
    if(!value.children || value.children.length===0){
        if(value.collapsed!== undefined){
            value.collapsed = true;
            return found;
          } 
        }
        for (var i = 0; i < value.children.length; i++) {
            if (value.children[i].label.toLowerCase().indexOf(filter) > -1) {
                //match found
                value.collapsed = false;
                value.children[i].collapsed = false;
                found = true;
            } else {
                //check child items
                if(value.children[i].children && value.children[i].children.length>0)
                {
                  if (findMatchingChildren(value.children[i].children, filter)) {
                    value.children[i].collapsed = false;
                    found = true;
                  }else{
                    value.children[i].collapsed = true;
                  } 
                }
            }
        }

        return found;
}

3. Create a function that loops through the children items of the current item, this function calls the function in step 2. You end up with a recursive function that checks all the levels.

function findMatchingChildren(children, filter) {
    var found = false;
    for (var i = 0; i < children.length; i++) {
        if (findMatch(children[i], filter)) {
            children[i].collapsed = false;
            found = true;
        }else{
            children[i].collapsed = true;
        }
    }
    return found;
 } 

As you find matching items you set the collapsed value to false

Please see working example here

HTML

<p id="expand-collapse-all">
    <a href="" ng-click="expandAll()">Expand all</a>
    <a href="" ng-click="collapseAll()">Collapse all</a>
</p>
Filter : <input ng-model="myFilter" type="text">
<ul frang-tree>
    <li frang-tree-repeat="node in treeData | filter:getFilter">
        <div>
            <span class="icon"
                ng-class="{collapsed: node.collapsed, expanded: !node.collapsed}"
                ng-show="node.children && node.children.length > 0"
                ng-click="node.collapsed = !node.collapsed"></span>
            <span class="label"
                ng-class="{folder: node.children && node.children.length > 0}"
                ng-bind="node.label"
                ng-click="action(node)"></span>
        </div>
        <ul ng-if="!node.collapsed && node.children && node.children.length>0" frang-tree-insert-children="node.children  | filter:myFilter"></ul>
 </li>
</ul>
like image 113
Tjaart van der Walt Avatar answered Oct 07 '22 20:10

Tjaart van der Walt