otc
otc

Reputation: 754

Apply filtering to a hierarchical structure

I have built the following Angular app that shows a hierarchy:

And I am trying to insert a text box on top of this hierarchy. To filter the data at the bottom. Have tried a couple of examples with filters but have not had much luck so far.

What I want to do is utilize angular binding when the user starts typing to the text box, dynamically expand and collapse the hierarchy and highlight the matches.

Looking for some advise on what is the best way to tackle this. Note the hierarchy can get big 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>

Edit: This is the filter I was thinking of but does not seem to adapt on the way ive structured the html: how to filter the data from text box in angularjs

If HTML structure needs to be changed i'm open to suggestions.

Upvotes: 3

Views: 1777

Answers (1)

hilnius
hilnius

Reputation: 2235

First, you should create a nested directive to display your tree. What if there is suddenly 7 levels to display ? So first I would write a recursive directive, which will also reduce the code size.

For the data filtering part, you can use an input with a ng-model-options="{debounce: 300}" combined with ng-change="filterFunction()" so the filtering applies only 300ms after the user has finished writing its search. The filterFunction() is pretty easy to write once your data is structured in a hierarchical form, and can change the object state to indicate to your directive wether it should be displayed, and wether its children should be 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>

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

I'll let you write the code part to highlight the text that matches the search, as it's pretty easy to transform the function that does the filtering. Hint: in the hierarchiesFilter() function, you can add a htmlHighlighted property to each node where you wrap the matching text between <strong> and </strong> tags.

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

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

Upvotes: 3

Related Questions