Apply filtering to hierarchical structure

I created the following Angular app which shows the hierarchy:

And I am trying to insert a textbox on top of this hierarchy. Filter data at the bottom. Have tried a couple of filter examples but so far no luck.

I want to use Angular binding, when the user starts typing a textbox, dynamically expand and collapse the hierarchy and highlight matches.

Looking for some tips on how best to handle this. Note that the hierarchy can get large and have around 3000 records.

angular.module('HelloWorldApp', [])
   .controller('HelloWorldController', function($scope) {

    $scope.mp6Root = [];
    $scope.mp6Data = [];

    var data = [
    {
        "cls": "L2-013551",
        "clsNm": "FASHION DOLLS",
        "subCt": "L3-001793",
        "subCtNm": "FASHION DOLLS AND ACCESSORIES",
        "ct": "L4-000429",
        "ctNm": "DOLLS GAMES PUZZLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006472",
        "clsNm": "FASHION DOLL WITH ACCS",
        "subCt": "L3-001793",
        "subCtNm": "FASHION DOLLS AND ACCESSORIES",
        "ct": "L4-000429",
        "ctNm": "DOLLS GAMES PUZZLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-014668",
        "clsNm": "ACTIVITIES",
        "subCt": "L3-001793",
        "subCtNm": "FASHION DOLLS AND ACCESSORIES",
        "ct": "L4-000429",
        "ctNm": "DOLLS GAMES PUZZLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-014667",
        "clsNm": "STORAGE",
        "subCt": "L3-001793",
        "subCtNm": "FASHION DOLLS AND ACCESSORIES",
        "ct": "L4-000429",
        "ctNm": "DOLLS GAMES PUZZLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-014675",
        "clsNm": "FASHION DOLL PLAYSET",
        "subCt": "L3-001793",
        "subCtNm": "FASHION DOLLS AND ACCESSORIES",
        "ct": "L4-000429",
        "ctNm": "DOLLS GAMES PUZZLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006476",
        "clsNm": "ROLE PLAY FASHION AND TOY",
        "subCt": "L3-001793",
        "subCtNm": "FASHION DOLLS AND ACCESSORIES",
        "ct": "L4-000429",
        "ctNm": "DOLLS GAMES PUZZLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-014677",
        "clsNm": "CORE PS FIGURE W PLAYSET",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006508",
        "clsNm": "CORE PS MUSICAL INSTRUMENT",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-014788",
        "clsNm": "WAGONS TOYS",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006536",
        "clsNm": "RIDING TOYS FOOT TO FLOOR",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-014678",
        "clsNm": "CORE PS PUZZLE",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006506",
        "clsNm": "CORE PS FIGURE PLAYSET",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006509",
        "clsNm": "CORE PS OTHER TOYS",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006511",
        "clsNm": "CORE PS TALKING SOUND",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006507",
        "clsNm": "CORE PS LEARNING TOY",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006510",
        "clsNm": "CORE PS ROLEPLAY",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006512",
        "clsNm": "CORE PS VEHICLES",
        "subCt": "L3-001798",
        "subCtNm": "CORE PRESCHOOL TOYS",
        "ct": "L4-000428",
        "ctNm": "PRESCHOOL",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006585",
        "clsNm": "DIECAST MED LG SCALE VEHICLES",
        "subCt": "L3-001818",
        "subCtNm": "DIECAST AND PLAYSETS",
        "ct": "L4-000425",
        "ctNm": "ACT FIGS CONSTRUCTION VEHICLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006587",
        "clsNm": "DIECAST PLAYSETS",
        "subCt": "L3-001818",
        "subCtNm": "DIECAST AND PLAYSETS",
        "ct": "L4-000425",
        "ctNm": "ACT FIGS CONSTRUCTION VEHICLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006586",
        "clsNm": "DIECAST MINI VEHICLES",
        "subCt": "L3-001818",
        "subCtNm": "DIECAST AND PLAYSETS",
        "ct": "L4-000425",
        "ctNm": "ACT FIGS CONSTRUCTION VEHICLES",
        "seg": "L5-000031",
        "segNm": "TOYS",
        "area": "L6-000004",
        "areaNm": "HARDLINES"
    },
    {
        "cls": "L2-006798",
        "clsNm": "VACUUMS UPRIGHT BAGLESS",
        "subCt": "L3-001851",
        "subCtNm": "FLOOR CLEANING",
        "ct": "L4-000449",
        "ctNm": "HOME ELECTRICS",
        "seg": "L5-000054",
        "segNm": "HARD HOME",
        "area": "L6-000012",
        "areaNm": "IN AND OUTDOOR HOME"
    },
    {
        "cls": "L2-006795",
        "clsNm": "VACUUMS HAND",
        "subCt": "L3-001851",
        "subCtNm": "FLOOR CLEANING",
        "ct": "L4-000449",
        "ctNm": "HOME ELECTRICS",
        "seg": "L5-000054",
        "segNm": "HARD HOME",
        "area": "L6-000012",
        "areaNm": "IN AND OUTDOOR HOME"
    },
    {
        "cls": "L2-006791",
        "clsNm": "FLOOR DEEP CLEANER CHEMICALS",
        "subCt": "L3-001851",
        "subCtNm": "FLOOR CLEANING",
        "ct": "L4-000449",
        "ctNm": "HOME ELECTRICS",
        "seg": "L5-000054",
        "segNm": "HARD HOME",
        "area": "L6-000012",
        "areaNm": "IN AND OUTDOOR HOME"
    },
    {
        "cls": "L2-006796",
        "clsNm": "VACUUMS STICK",
        "subCt": "L3-001851",
        "subCtNm": "FLOOR CLEANING",
        "ct": "L4-000449",
        "ctNm": "HOME ELECTRICS",
        "seg": "L5-000054",
        "segNm": "HARD HOME",
        "area": "L6-000012",
        "areaNm": "IN AND OUTDOOR HOME"
    },
    {
        "cls": "L2-012895",
        "clsNm": "FLOOR STEAM MOPS",
        "subCt": "L3-001851",
        "subCtNm": "FLOOR CLEANING",
        "ct": "L4-000449",
        "ctNm": "HOME ELECTRICS",
        "seg": "L5-000054",
        "segNm": "HARD HOME",
        "area": "L6-000012",
        "areaNm": "IN AND OUTDOOR HOME"
    }]
    ;
        
    $scope.loadMP6DataToMemory = function(data) {

        angular.forEach(data, function (value, key) {

            if ($.inArray(value.area, $scope.mp6Root) === -1) {
                $scope.mp6Root.push(value.area);
            }
            
            addToMap(value.cls, value.clsNm, "");
            addToMap(value.subCt, value.subCtNm, value.cls);
            addToMap(value.ct, value.ctNm, value.subCt);
            addToMap(value.seg, value.segNm, value.ct);
            addToMap(value.area, value.areaNm, value.seg);
        });
    }

    addToMap = function (pKey, pName, pChild) {
        if (!$scope.mp6Data[pKey]) {
            cSet = [];
            $scope.mp6Data[pKey] = { name: pName, children: cSet };
        } else {
            if ($.inArray(pChild, $scope.mp6Data[pKey].children) === -1) {
                $scope.mp6Data[pKey].children.push(pChild);
            }
        }
    }

    $scope.ExpandMP6 = function (pKey) {
        if (pKey) {
            mp = $scope.mp6Data[pKey];
            return {
                name: mp.name,
                children: mp.children,
                visible: false
            }
        }
    }


    $scope.loadMP6DataToMemory(data);

    $scope.l5visible = false;
    $scope.l4visible = false;
    $scope.l3visible = false;
    $scope.l2visible = false;

});
      

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class="container" ng-app="HelloWorldApp" ng-controller="HelloWorldController">

            <div class="md-grid">
                <ul class="md-list">
                    <li class="md-list-item-text" ng:repeat="l6 in mp6Root" ng-click="l5visible = !l5visible; $event.stopPropagation();">
                        L6 {{ExpandMP6(l6).name}}
                        <ul class="md-list" ng-show="l5visible">
                            <li class="md-list-item-text" ng:repeat="l5 in ExpandMP6(l6).children" ng-click="l4visible = !l4visible; $event.stopPropagation();">
                                L5 {{ExpandMP6(l5).name}}
                                <ul class="md-list" ng-show="l4visible">
                                    <li class="md-list-item-text" ng:repeat="l4 in ExpandMP6(l5).children" ng-click="l3visible = !l3visible; $event.stopPropagation();">
                                        L4 {{ExpandMP6(l4).name}}
                                        <ul class="md-list" ng-show="l3visible">
                                            <li class="md-list-item-text" ng:repeat="l3 in ExpandMP6(l4).children" ng-click="l2visible = !l2visible; $event.stopPropagation();">
                                                L3 {{ExpandMP6(l3).name}}
                                                <ul class="md-list" ng-show="l2visible">
                                                    <li class="md-list-item-text" ng:repeat="l2 in ExpandMP6(l3).children">
                                                        L2 {{ExpandMP6(l2).name}}
                                                    </li>
                                                </ul>
                                            </li>
                                        </ul>
                                    </li>
                                </ul>
                            </li>
                        </ul>
                    </li>
                </ul>
            </div>
            </div>
      

Run codeHide result


Edit: This is the filter I was thinking of but doesn't seem to adapt to the way Iv structured the html: how to filter data from a textbox in angularjs

If the HTML structure needs to be changed, I am open to suggestions.

+3


source to share


1 answer


First you have to create a nested directive to display your tree. What if 7 levels appear on the display? So first I would write a recursive directive that will also reduce the size of the code.

For the data filtering part, you can use input

with ng-model-options="{debounce: 300}"

in combination with ng-change="filterFunction()"

, so the filtering only applies to 300ms after the user finishes recording their search. filterFunction()

it's pretty easy to write once your data is structured in a hierarchical fashion and can change the state of the object, pointing to your directive so that it is displayed and that its children are displayed.

The result looks like this:

MainController.js

var app = angular.module('app', []);
app.controller('MainController', [function () {
  var ctrl = this;
  ctrl.search = '';
  initHierarchies(); // function that transforms the data in hierarchical form
  // filterHierarchies is called everytime the user changed the search input
  ctrl.filterHierarchies = function () {
    ctrl.filteredHierarchies = hierarchiesFilter(ctrl.hierarchies, ctrl.search).hierarchies;
  }
  ctrl.filterHierarchies(); // init the filteredHierarchies data.

  // function that filters the hierarchy. It is a recursive function
  function hierarchiesFilter(hierarchies, search) {
    if (!hierarchies || !hierarchies.length) {
      return { hierarchies: [], hasExpandedChildren: false};
    }
    console.log(hierarchies, search);
    var oneIsExpanded = false;
    for (var i = 0; i < hierarchies.length; i++) {
      hierarchies[i].showChildren = false;
      if (search.length) {
        var rx = new RegExp(search, 'i');
        if (hierarchies[i].name.match(rx)) {
          oneIsExpanded = true;
        }
      }
      // if the node has children which are expanded, we need to display it so its children that
      // should be highlighted are visible
      var hasExpandedChildren = hierarchiesFilter(hierarchies[i].children, search).hasExpandedChildren;
      if (hasExpandedChildren) {
        hierarchies[i].showChildren = true;
        oneIsExpanded = true;
      }
    }
    return { hierarchies: hierarchies, hasExpandedChildren: oneIsExpanded };
  };

  // function to transform the array data to a hierarchical structure
  function initHierarchies() {
    var data = getData();
    var mp6Data = {};
    var mp6Root = [];

    angular.forEach(data, function (value, key) {
        if (mp6Root.indexOf(value.area) === -1) {
            mp6Root.push(value.area);
        }

        addToMap(value.cls, value.clsNm, "");
        addToMap(value.subCt, value.subCtNm, value.cls);
        addToMap(value.ct, value.ctNm, value.subCt);
        addToMap(value.seg, value.segNm, value.ct);
        addToMap(value.area, value.areaNm, value.seg);
    });

    function addToMap(pKey, pName, pChild) {
        if (!mp6Data[pKey]) {
            mp6Data[pKey] = { name: pName, childrenKeys: [] };
        } else {
            if (mp6Data[pKey].childrenKeys.indexOf(pChild) === -1) {
                mp6Data[pKey].childrenKeys.push(pChild);
            }
        }
    }

    function buildHierarchicalStructure(childrenKeys) {
      var builtChildren = [];
      for (var i = 0; i < childrenKeys.length; i++) {
        builtChildren.push({
          name: mp6Data[childrenKeys[i]].name,
          children: buildHierarchicalStructure(mp6Data[childrenKeys[i]].childrenKeys)
        });
      }
      return builtChildren;
    }

    for (var i = 0; i < mp6Data.length; i++) {
      mp6Data[i].showChildren = true;
    }
    ctrl.hierarchies = buildHierarchicalStructure(mp6Root);
  }
}]);

      

hierarchy.directive.js

app.directive('hierarchy', ['RecursionHelper', function (RecursionHelper) {
  return {
    template: '<div><div ng-click="hierarchyCtrl.ngModel.showChildren = !hierarchyCtrl.ngModel.showChildren">{{ hierarchyCtrl.ngModel.name }}</div><ul ng-if="hierarchyCtrl.ngModel.children && (hierarchyCtrl.ngModel.showChildren)"><li ng-repeat="element in hierarchyCtrl.ngModel.children"><hierarchy ng-model="element"></hierarchy></li></ul></div>',
    restrict: 'E',
    scope: { ngModel: '=' },
    controller: ['$scope', function($scope) { this.ngModel = $scope.ngModel; }],
    controllerAs: 'hierarchyCtrl',
    compile: function (element) { 
      return RecursionHelper.compile(element);
    },
  };
}]);

      



index.html

<body>
<h1>Hello Plunker!</h1>
<div ng-controller="MainController as mainCtrl">
  <input type="text" ng-model="mainCtrl.search" ng-model-options="{debounce: 300}" ng-change="mainCtrl.filterHierarchies()" />
  <ul>
    <li ng-repeat="hierarchy in mainCtrl.filteredHierarchies"><hierarchy ng-model="hierarchy"></hierarchy></li>
  </ul>
</div>
</body>

      

Plunger example: https://plnkr.co/edit/1jiiiwkdUZY4tm7sm79F?p=preview

I'll let you write a piece of code to highlight the text that matches the search, since it's pretty easy to transform the function that does the filtering. Hint: in a function, hierarchiesFilter()

you can add a property htmlHighlighted

for each node where you wrap the corresponding text between the <strong>

and tags </strong>

.

Since this is probably not the exact behavior you were looking for, you can adjust the filter function to display exactly what you want when the user changes their search.

Some code follows from this post (recursion helper): Recursion in Angular directives

+3


source







All Articles