byCoder
byCoder

Reputation: 9184

AngularJS: filter in controller on all field except one field

In my angularjs app i get via json such example of data:

{"id":"1a", "name": "aaa", "emails": {{"[email protected]"}, {"[email protected]"}}},
{"id":"2a", "name": "aba", "emails": {{"[email protected]"}, {"[email protected]"}}},
{"id":"3a", "name": "aab", "emails": {{"[email protected]"}, {"[email protected]"}}},

and for performance reasons i didn't use filter for ng-repeat, but use ng-show schema...

so in controller i have such code (search is my inputs value):

  $scope.$watch('search', function(newVal, oldVal) {
    $scope.filteredArray = $filter('filter')($scope.users, $scope.search, newVal);
  });

but it search even in id's, for example i enter a and get it from id, but i do not need field id here...

So how to search with filter only in specific fields?

Upvotes: 4

Views: 4375

Answers (5)

ornic
ornic

Reputation: 382

If you can change your field name to something starting with $, then Angular will skip this field in filtering. Compare https://next.plnkr.co/edit/TjSdGICBfcR8WDyV with original example from https://docs.angularjs.org/api/ng/filter/filter. I added $ to name and now | filter skips field $name in global filtering using search.$.

Upvotes: 0

Vadim
Vadim

Reputation: 8789

If you care about performance, probably the best choice would be not to use $filter('filter') at all. Assuming your data is a JSON you can do the following:

function contains(src, value, except) {
  var key;
  switch(typeof src) {
    case 'string':
    case 'number':
    case 'boolean':
      return String(src).indexOf(value) > -1;
    case 'object':
      except = except || [];
      for(key in src) {
        if( src.hasOwnProperty(key) &&
            except.indexOf(key) < 0 &&
            contains(src[key], value, except)
        ) {
          return true;
        }
      }
  }
  return false;
}

angular.module('app', []).
  factory('appService', function() {
    var data = [
      {"id":"1a", "name": "aaa", "emails": ["[email protected]", "[email protected]"]},
      {"id":"2a", "name": "aba", "emails": ["[email protected]", "[email protected]"]},
      {"id":"3a", "name": "aab", "emails": ["[email protected]", "[email protected]", ["[email protected]", "[email protected]"]]}
    ];
    return {
      getFilteredData: function(filter, except) {
        return filter ? data.filter(function(item) {
          return contains(item, filter, except)
        }) : data.slice(0);
      }
    }
  }).
  controller('appController', ['$scope', 'appService', function($scope, appService) {
    $scope.search = '';
    $scope.$watch('search', function(val) {
      $scope.data = appService.getFilteredData(val, ['id']);
    });
  }]);
<!DOCTYPE html>
<html ng-app="app">

  <head>
    <script src="https://code.angularjs.org/1.4.0-beta.6/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body ng-controller="appController">
    <input ng-model="search" ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"/>
    <ul>
      <li ng-repeat="item in data">{{::item}}</li>
    </ul>
  </body>

</html>

Code above will make a deep search in all fields of data and it's items excluding list of fields passed as parameter (in this example ['id'])

Tip #1: Implementing business logic inside controller is kind of bad practice in Angular, which has services for that purpose.

Tip #2: If you have large data-sets to be displayed, debouncing filter change will be very beneficial from performance point of view.

Tip #3: If your data-set items are immutable, you can take advantage of one-time binding (using {{::item}} syntax) and speed-up your app by reducing number of watches created by Angular.

Upvotes: 6

Manube
Manube

Reputation: 5242

Several answers have been given already, and great tips were given. My contribution will not summarise the work of others, but just mention an alternate way to filter data with angularjs, with performance in mind: using an extremely fast, third-party filtering tool.


Yes, having filters in the ng-repeat with x in data | myFilter is great for readability, but performs poorly with a large collection, as the filter is evaluated repeatedly.

With large collections, it is best to trade readability for performance, and filter in the controller, like mentioned in the OP.


Now if you are going to filter in the controller for performance's sake, you might as well not re-invent the wheel, and use existing filtering tools.

Here is a plunker with a simple toolbox, filtering instantly a collection of a half-million documents.

I wrote it several months ago, as the filtering of a collection of 3000+ documents was slowing down my app when I used either custom or built-in angularjs filters.

filter a large collection using an extremely fast third-party tool: crossFilter

Now enter a number in the box: the filtering is (almost) instant. In fact, the reason it is not instant is because I introduced a 750ms delay on purpose, to give time to the user to type, and not filter at every key press: var filterTextTimeout=750;

EDIT: as rightly mentioned in the comments, with angularjs versions >1.2, the debounce option of ng-model should be used instead of $timeout


The filtering tool used is crossFilter, along with ng-crossfilter.

The reason it can be so fast is that it creates (very quickly) its own sorted indexes.

It comes up with many features, like RegExp, InArray Filter, fuzzy filters, map-reduce, etc...

Upvotes: 2

zs2020
zs2020

Reputation: 54524

You can customize the filter expression by calling filter function on each field you are interested in.

angular.module("app", []).controller("ctrl", ["$scope", "$filter", function ($scope, $filter) {
    $scope.users = [{
        "id": "1a",
            "name": "aaa",
            "emails": ["[email protected]", "[email protected]"]
    }, {
        "id": "2a",
            "name": "aba",
            "emails": ["[email protected]", "[email protected]"]
    }, {
        "id": "3a",
            "name": "aab",
            "emails": ["[email protected]", "[email protected]"]
    }];

    $scope.$watch('search', function (newVal, oldVal) {
        //use function to implement a specific expression
        var expression = function (value, index) {
            fields = ['name', 'emails'];
            for (var i in fields) {
                field = fields[i];
                query = {}; //create an expression
                query[field] = $scope.search;
                r = $filter('filter')([value], query);
                if (r.length > 0) return true;
            }
            return false;
        }
        $scope.filteredArray = $filter('filter')($scope.users, expression)
    });
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
    <div ng-controller="ctrl">Search:
        <input type="text" ng-model="search">
        <div ng-repeat="x in filteredArray">{{x}}</div>
    </div>
</div>
  

Upvotes: 1

The comparator for the filter can be an object that mirrors the objects in your array. If this is the case, it will only filter based on the exiting properties. That is to say if newVal is an object of the following form:

{
  name: someVal,
  emails: otherVal
}

You will only filter on name and emails, not id. Having an id field which is blank is also fine (that just means you wont search by id).

Here is an example:

angular.module("app", []).controller("ctrl", ["$scope", "$filter", function($scope, $filter){
  $scope.data = [
    {"id":"1a", "name": "aaa", "emails": ["[email protected]", "[email protected]"]},
    {"id":"2a", "name": "aba", "emails": ["[email protected]", "[email protected]"]},
    {"id":"3a", "name": "aab", "emails": ["[email protected]", "[email protected]"]}
  ];
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
  <div ng-controller="ctrl">
    Name: <input type="text" ng-model="filter.name"><br>
    Email: <input type="text" ng-model="filter.emails">
    <div ng-repeat="x in data | filter:filter">
      {{x}}
    </div>
  </div>
</div> 
  

Note how entering a value in the emails input will search the array properly. if you want you can also set both inputs to the same model, and then set that model to filter.name and filter.email.

//html
Search Query: <input type="text" ng-model="query">
//js
$scope.$watch('query', function(newVal, oldVal){
    $scope.filter.name=newVal;
    $scope.filter.emails=newVal;
}

Upvotes: -1

Related Questions