Filtering on object map rather than array in AngularJS

Given a controller with a $scope property that is an object with other properties rather than an array like below, how should I filter the ng-repeat set?

Here is a JSFiddle: http://jsfiddle.net/ZfGx4/110/

Controller:

function HelloCntl($scope, $filter) {
    $scope.friends = {
        john: {
            name: 'John',
            phone: '555-1276'
        },
        mary: {
            name: 'Mary',
            phone: '800-BIG-MARY'
        },
        mike: {
            name: 'Mike',
            phone: '555-4321'
        },
        adam: {
            name: 'Adam',
            phone: '555-5678'
        },
        julie: {
            name: 'Julie',
            phone: '555-8765'
        }
    };
}?

Template:

<div ng:app>
 <div ng-controller="HelloCntl">
  <input placeholder="Type to filter" ng-model="query">     
  <ul>
   <li ng-repeat="(id, friend) in friends | filter:query">
    <span>{{friend.name}} @ {{friend.phone}}</span>
   </li>
  </ul>
 </div>
</div>

Answers:

Answer

I would change my data structure to an array. Anyway, here's another implementation to filter your friends object.

angular.module('filters',['utils'])
  .filter('friendFilter', function(utils){

    return function(input, query){
      if(!query) return input;
      var result = [];

      angular.forEach(input, function(friend){
        if(utils.compareStr(friend.name, query) ||
           utils.compareStr(friend.phone, query))
          result.push(friend);          
      });
      return result;
    };
  });

This iterates over the object only once, compares by name and phone and can be called like this.

<li ng-repeat="friend in friends | friendFilter:query">

I defined the compareStr in another module, but you don't really need to do it.

angular.module('utils', [])
  .factory('utils', function(){
    return{
      compareStr: function(stra, strb){
        stra = ("" + stra).toLowerCase();
        strb = ("" + strb).toLowerCase();
        return stra.indexOf(strb) !== -1;
      }
    };
  });

Don't forget to inject the filters module into your app

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

Here's the full example: http://jsbin.com/acagag/5/edit

Answer

I guess you can't do it directly with 'filter'. Looking at the code in angular.js, these are the first lines of the filter function:

function filterFilter() {
  return function(array, expression) {
    if (!(array instanceof Array)) return array;

So if it receives something different from an array, it does nothing.

Here is one way to do it, not sure if I would recommend it, but it's is an idea:

In the controller, just convert to an array before passing it to the filter:

$scope.filteredFriends = function() {
    var array = [];
    for(key in $scope.friends) {
        array.push($scope.friends[key]);
    }
    return $filter('filter')(array, $scope.query);
}

And the ng-repeat:

<li ng-repeat="friend in filteredFriends()">

Example: http://jsbin.com/acagag/2/edit

Maybe a better solution is to write a custom filter.

Answer

I had the same problem, and succeeded by creating my own filter that accepts a map, while still using the built-in filter for doing the actual matching.

My custom filter traverses the map, and for each element calls the built-in filter, but given that that filter only accepts an array, I wrap each element in an array of length 1 (the [data] below). So the match succeeds if the output-array's length is still 1.

.filter('mapFilter', function($filter) {
  var filter = $filter('filter');

  return function(map, expression, comparator) {
    if (! expression) return map;

    var result = {};
    angular.forEach(map, function(data, index) {
      if (filter([data], expression, comparator).length)
        result[index] = data;          
    });

    return result;
  }
})

This setup loses efficiency of course because the built-in filter is required to determine what it needs to do for each element in the map, instead of only once if you give it an array with all elements, but in my case, with a map of 500 elements the filtering is still instanteanous.

Answer

I made an example on how to not change your object to an Array. I think this answers the question more correct.

I had the same problem that i could not search in a Object.

http://jsbin.com/acagag/223/edit

angular.module('filters',['utils'])
  .filter('friendFilter', function(utils){

    return function(input, query){
      if(!query) return input;
      var result = {};

      angular.forEach(input, function(friendData, friend){
        if(utils.compareStr(friend, query) ||
           utils.compareStr(friendData.phone, query))
          result[friend] = friendData;          
      });
      return result;
    };
  });

So just instead of returning a array you return a object.

Hope this helps someone!

Answer

Not an optimal solution but if you want something quick and dirty :

<li ng-repeat="(id, friend) in friends | filter:query" ng-hide="id !== query.id">
  <span>{{friend.name}} @ {{friend.phone}}</span>
</li>

Or ng-if instead of ng-hide

Answer

Rather than using a filter you can also simply :

$http.get('api/users').success(function(data){
    angular.forEach(data, function(value, key){
        users.push(value);
    });
    $scope.users = users;
});

And in the template

<input type="text" ng-model="query" placeholder="Search user"/>

<li ng-repeat="user in users | filter:query">
    {{ user.name }}
</li>

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.