Фильтр AngularJS не расширяется отфильтрованных узлов

Я использую https://github.com/tchatel/angular-treeRepeat и пытаюсь фильтровать узлы, которые не были расширены. Поэтому я изменил этот код, включив фильтр AngularJS:

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>

Это работает, как ожидается, если все узлы дерева будут расширены: Строка 20 на контроллерах .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 } ] } ] } ]");

Но если узлы свернуты, то согласованные узлы будут расширены/видны. Дерево остается рухнувшим. Конфигурация для свернутых узлов: Строка 21 на контроллерах .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

Нужно ли вручную разворачивать узлы по типу пользователя или существует ли конфигурация angular, которую я могу использовать для расширения этих узлов?

Я попытался добавить пользовательскую функцию, которая запускает пользовательские типы каждый раз:

  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);

  });
  } 
}

Но это очень неэффективно, поскольку оно перемещает всю структуру дерева для каждого типа пользовательских типов. Это также работает только для точно подобранного текста: searchText.toLowerCase() === childNode.label.toLowerCase(). Попробуйте использовать contains вместо === без успеха.

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>

Ответы

Ответ 1

Вы можете добиться этого, используя фильтр с функцией предикат.

function (value, index): функцию предикатов можно использовать для записи произвольных фильтров. Функция вызывается для каждого элемента массива. Конечным результатом является массив из тех элементов, которые предикат вернул true.

  • Создать предикатную функцию

$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. Создайте функцию, которая проверяет соответствие текущего элемента (эта функция вызывается из функции предиката)
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. Создайте функцию, которая пересекает элементы child текущего элемента, эта функция вызывает функцию на шаге 2. Вы получаете рекурсивную функцию, которая проверяет все уровни.

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;
 } 

Когда вы найдете подходящие элементы, вы установите для параметра collapsed значение false

См. рабочий пример здесь

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>

Ответ 2

Рассмотрите возможность отладки модуля angular-treeRepeat в целом и используя рекурсивный шаблон. Бен Фостер объясняет, как он работает в этом сообщении , и это гораздо более изящное (и менее безумно сложное) решение, я думаю.

Рабочий пример с фильтром: JSFiddle

HTML:

<div ng-app="app" ng-controller='AppCtrl'>
  Filter: <input ng-model="myFilter" type="text">
  <script type="text/ng-template" id="categoryTree">
    {{ category.title }}
    <ul ng-show="category.categories">
      <li
        ng-repeat="category in category.categories | filter:myFilter"
        ng-include="'categoryTree'">
      </li>
    </ul>
  </script>
  <ul>
    <li
      ng-repeat="category in categories | filter:myFilter"
      ng-include="'categoryTree'">
    </li>
  </ul>
</div>

JavaScript:

var app = angular.module('app', [])
  .controller('AppCtrl', function($scope) {
    $scope.categories = [{
      title: 'Computers',
      categories: [{
          title: 'Laptops',
          categories: [{
            title: 'Ultrabooks'
          },{
            title: 'Macbooks'
          }]
        },{
          title: 'Desktops'
        },{
          title: 'Tablets',
          categories: [{
            title: 'Apple'
          },{
            title: 'Android'
          }]
        }
      ]
    },{
      title: 'Printers'
    }];
  });