Filtering nested ng-repeat: hide parents who have no children

I want to make some kind of project list from a JSON file. The data structure (year, month, project) looks like this:

[{
    "name": "2013",
    "months": [{
        "name": "May 2013",
        "projects": [{
            "name": "2013-05-09 Project A"
        }, {
            "name": "2013-05-14 Project B"
        }, { ... }]
    }, { ... }]
}, { ... }]

      

I am displaying all data with a nested ng-repeat

and making it searchable by a filter related to the query from the input field.

<input type="search" ng-model="query" placeholder="Suchen..." />

<div class="year" ng-repeat="year in data | orderBy:'name':true">
     <h1>{{year.name}}</h1>

    <div class="month" ng-repeat="month in year.months | orderBy:sortMonth:true">
         <h3>{{month.name}}</h3>

        <div class="project" ng-repeat="project in month.projects | filter:query | orderBy:'name'">
            <p>{{project.name}}</p>
        </div>
    </div>
</div>

      

If I find "Project B" now, all empty parent elements are still visible. How can I hide them? I've tried some tricks ng-show

, but the main problem seems to be that I don't have access to any information about the parent's filtered state.

Here is a fiddle demonstrating my problem: http://jsfiddle.net/stekhn/y3ft0cwn/7/

+3


source to share


4 answers


I think the best way is to implement a custom function to update the custom array with the filtered data whenever it changes query

. Like this:

$scope.query = '';
$scope.filteredData= angular.copy($scope.data);
$scope.updateFilteredData = function(newVal){
    var filtered = angular.copy($scope.data);
    filtered = filtered.map(function(year){ 
        year.children=year.children.map(function(month){
            month.children = $filter('filter')(month.children,newVal);
            return month;
        });
        return year;
    });
    $scope.filteredData = filtered.filter(function(year){
        year.children= year.children.filter(function(month){
            return month.children.length>0;
        });
        return year.children.length>0;
    });
}

      

And then your view will look like this:

    <input type="search" ng-model="query" ng-change="updateFilteredData(query)"
           placeholder="Search..." />
    <div class="year" ng-repeat="year in filteredData | orderBy:'name':true">
         <h1>{{year.name}}</h1>
        <div class="month" ng-repeat="month in year.children | orderBy:sortMonth:true">
            <h3>{{month.name}}</h3>
            <div class="project" ng-repeat="project in month.children | orderBy:'name'">
                <p>{{project.name}}</p>
            </div>
        </div>
    </div>

      

Example




Why not custom $filter

for this?

Efficiency : The nature of the cycle $diggest

will make it much less efficient. The only problem is that this solution won't be as easy to replicate as a custom one $filter

. However, this custom $filter

one will also not be very reusable as its logic will be very dependent on this particular data structure.




IE8 support

If you need this to work in IE8, you'll either have to use jQuery to replace functions filter

and map

make sure those functions are defined like this: (By the way: if you need IE8 support, there's absolutely nothing wrong with using jQuery for this sort of thing. )

filter

if (!Array.prototype.filter) {
  Array.prototype.filter = function(fun/*, thisArg*/) {
    'use strict';

    if (this === void 0 || this === null) {
      throw new TypeError();
    }

    var t = Object(this);
    var len = t.length >>> 0;
    if (typeof fun !== 'function') {
      throw new TypeError();
    }

    var res = [];
    var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
    for (var i = 0; i < len; i++) {
      if (i in t) {
        var val = t[i];
        if (fun.call(thisArg, val, i, t)) {
          res.push(val);
        }
      }
    }
    return res;
  };
}

      

map

if (!Array.prototype.map) {
  Array.prototype.map = function(callback, thisArg) {
    var T, A, k;
    if (this == null) {
      throw new TypeError(" this is null or not defined");
    }
    var O = Object(this);
    var len = O.length >>> 0;
    if (typeof callback !== "function") {
      throw new TypeError(callback + " is not a function");
    }
    if (thisArg) {
      T = thisArg;
    }
    A = new Array(len);
    k = 0;
    while(k < len) {
      var kValue, mappedValue;
      if (k in O) {
        kValue = O[ k ];
        mappedValue = callback.call(T, kValue, k, O);
        A[ k ] = mappedValue;
      }
      k++;
    }
    return A;
  };      
}

      




Acknowledgment

I want to thank JB Nizet for his feedback .

+2


source


Basically you have to filter the months to keep only those that have at least one filtered project, and you also have to filter the years to keep only those that have at least one filtered month.

This can be easily achieved with the following code:

function MainCtrl($scope, $filter) {

    $scope.query = '';
    $scope.monthHasVisibleProject = function(month) {
        return $filter('filter')(month.children, $scope.query).length > 0;
    };

    $scope.yearHasVisibleMonth = function(year) {
        return $filter('filter')(year.children, $scope.monthHasVisibleProject).length > 0;
    };

      



and in the view:

<div class="year" ng-repeat="year in data | filter:yearHasVisibleMonth | orderBy:'name':true">
             <h1>{{year.name}}</h1>

            <div class="month" ng-repeat="month in year.children | filter:monthHasVisibleProject | orderBy:sortMonth:true">

      

This is quite inefficient, because to find out if a year is accepted, you filter all of its months, and each month, you filter all of its projects. Thus, if the performance is not enough for your amount of data, you should probably apply the same principle, but keeping the accepted / rejected state of each object (project, then month, year) every time the request changes.

+3


source


For those interested: I found another approach yesterday to solve this problem, which seems pretty ineffective to me. The functions are called again for each child as the query is typed. Not as nice as Josep's solution.

function MainCtrl($scope) {

    $scope.query = '';

    $scope.searchString = function () {

        return function (item) {
            var string = JSON.stringify(item).toLowerCase();
            var words = $scope.query.toLowerCase();

            if (words) {
                var filterBy = words.split(/\s+/);

                if (!filterBy.length) {
                    return true;
                }
            } else {
                return true;
            }

            return filterBy.every(function (word) {
                var exists = string.indexOf(word);

                if(exists !== -1){
                    return true;
                }
            });
        };
    };
};

      

And in the view:   

<div class="year" ng-repeat="year in data | filter:searchString() | orderBy:'name':true">
     <h1>{{year.name}}</h1>

    <div class="month" ng-repeat="month in year.children | filter:searchString() | orderBy:sortMonth:true">
         <h3>{{month.name}}</h3>

        <div class="project" ng-repeat="project in month.children | filter:searchString() | orderBy:'name'">
            <p>{{project.name}}</p>
        </div>
    </div>
</div>

      

Here is a fiddle: http://jsfiddle.net/stekhn/stv55sxg/1/

0


source


Doesn't this work? Using a filtered variable and checking its length.

<input type="search" ng-model="query" placeholder="Suchen..." />

    <div class="year" ng-repeat="year in data | orderBy:'name':true" ng-show="filtered.length != 0">
     <h1>{{year.name}}</h1>

    <div class="month" ng-repeat="month in year.months | orderBy:sortMonth:true">
         <h3>{{month.name}}</h3>

        <div class="project" ng-repeat="project in filtered = (month.projects | filter:query) | orderBy:'name'">
            <p>{{project.name}}</p>
        </div>
    </div>
</div>

      

0


source







All Articles