Angular JS - Creating a filter to compare 2 arrays

I am developing a search page for an application. I have searched a lot at google, but I can't find anything than fragmented information about filters.

What I need is a list of checkboxes containing certain lists of elements (like in the example, languages). I had put a filter for ordering, for the name of the client, but I want to add filters to auxiliar tables like (as I said) languages.

Clients:

$scope.clientes = [
    { 'id': '3', 'name': 'Susana', 'surname': 'Rodríguez Torrón', 'languages': [{'id': 1, 'name': 'english'}, {'id': 2, 'name': 'spanish'}] },
    { 'id': '4', 'name': 'Pablo', 'surname': 'Campos Pérez', 'languages': [{'id': 3, 'name': 'german'}, {'id': 5, 'name': 'japanese'}] }
];

Languages:

$langs = [
     {'id': 1, 'name': 'english' },
     {'id': 2, 'name': 'spanish' },
     {'id': 3, 'name': 'german' },
     {'id': 4, 'name': 'japanese' }
];

HTML (for checkboxes list):

<div class="row">
  <div class="col-md-12">
    <h4>Languages</h4>
    <div class="checkbox-block" ng-repeat="lang in langs">
      <label class="checkbox" for="{{lang.id}}">
        <input type="checkbox" ng-model="langs[lang.id]" name="languages_group" id="{{lang.id}}" />
        <span ng-bind-html="lang.name"></span>
      </label>
    </div>
  </div>
</div>;

Clients:

<div class="client-wrapper" ng-repeat="client in clients | orderBy:order_field:order_type | filter:query">
    ... all the data of every client showed here ...
</div>;

In the array $scope.langs I havethe checks checked, now I want to compare it with every client and to show only the clients having that language(s). Can it be done? Can you help me to know how??

EDIT: Code of the filter.

    app.filter('langsFilter', function () {
        return function (input, $scope) {
            var output = [];
            to_langs = $scope.filters.langs;


            var trueContro = false;
            for (var lang in to_langs) {
                if (to_langs[lang] === true) {
                    trueContro = true;
                }
            }

            for (var client in input) {
                for (var i = 0; i < input[client].langs.length; i++) {
                    if (trueContro === true) {
                        if (to_langs[client.langs[i]]) {
                            output.push(input[client]);
                        }
                    } else {
                        output.push(input[client]);
                    }
                }
            }

            return output;
        };
    });

As you said I posted the code I am working at. It doesn't works right now, I know, but It will give you an idea of what I need to achieve:

We have a set of clients, in addition to another filters, this filter will compare the langs of every client for show only the client with those langs.

Answers:

Answer

(Caveat: for simplicity's sake I've stuffed everything into a directive here; in real life much of this code would belong in a controller or elsewhere)

DON'T DO THIS

This sample shows a filter working as you describe. I had to change your ng-model -- putting ng-model="langs[lang.id]" on a checkbox overwrites the data you had in the langs array with the box's "checked" status, so instead I tied the user's language selections to scope.selectedLangs -- but it otherwise uses your existing data structure.

var app = angular.module("app", []);
app.directive('sampleDirective', function() {
  return {
    restrict: 'A',
    link: function(scope) {
      scope.langs = [
        {'id': 1, 'name': 'english'}, 
        {'id': 2, 'name': 'spanish'},
        {'id': 3, 'name': 'german'}, 
        {'id': 4, 'name': 'japanese'}
      ];
      scope.selectedLangs = {};

      scope.clientes = [{
        'id': '3',
        'name': 'Susana',
        'surname': 'Rodríguez Torrón',
        'languages': [
          {'id': 1, 'name': 'english'}, 
          {'id': 2, 'name': 'spanish'}
        ]
      }, {
        'id': '4',
        'name': 'Pablo',
        'surname': 'Campos Pérez',
        'languages': [
          {'id': 3, 'name': 'german'}, 
          {'id': 4, 'name': 'japanese'}
        ]
      }];
    }
  };
});

app.filter('sampleFilter', function() {
  return function(clientes, selectedLangs) {
    var ret = [];
    angular.forEach(clientes, function(client) {
      var match = false;
      angular.forEach(client.languages, function(l) {
        if (selectedLangs[l.id]) {
          match = true;
        }
      });
      if (match) {
        ret.push(client);
      }
    });
    return ret;
  };
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
  <div sample-directive>
    Languages:
    <div ng-repeat="lang in langs">
      <label>
        <input type="checkbox" ng-model="selectedLangs[lang.id]" name="languages_group">{{lang.name}}
      </label>
    </div>
    <br>Clients:
    <div ng-repeat="client in clientes | sampleFilter:selectedLangs">{{client.name}} {{client.surname}}
    </div>
  </div>
</div>

But please consider not doing it this way -- angular filters are not very performant by nature (and this is exacerbated by the inefficient data structure you've chosen for both languages and clients. You should at the very least think about changing your arrays-of-objects-with-IDs into hash tables keyed by those IDs, i.e:

scope.langs = {
  1: {name: 'english'},
  2: {name: 'spanish'}
  // ...etc
}

This would save you having to iterate through so many nested loops to check client languages against available languages -- the way you have it now is the worst of both worlds, because if you need to find anything by ID you still have to iterate through the array.)

DO THIS INSTEAD

Instead of depending on a filter which will run every $digest, you'll be better off watching for changes to the selected languages and updating your results only when needed, like so:

var app = angular.module("app", []);

app.directive('sampleDirective', function() {
  return {
    restrict: 'A',
    link: function(scope) {
      scope.langs = [
        {'id': 1, 'name': 'english'}, 
        {'id': 2, 'name': 'spanish'},
        {'id': 3, 'name': 'german'}, 
        {'id': 4, 'name': 'japanese'}
      ];

      scope.clientes = [{
        'id': '3',
        'name': 'Susana',
        'surname': 'Rodríguez Torrón',
        'languages': [
          {'id': 1, 'name': 'english'}, 
          {'id': 2, 'name': 'spanish'}
        ]
      }, {
        'id': '4',
        'name': 'Pablo',
        'surname': 'Campos Pérez',
        'languages': [
          {'id': 3, 'name': 'german'}, 
          {'id': 4, 'name': 'japanese'}
        ]
      }];

      scope.selectedLangs = {};

      scope.filterByLanguage = function() {
        scope.matchedClients = [];
        angular.forEach(scope.clientes, function(client) {
          var match = false;
          angular.forEach(client.languages, function(l) {
            if (scope.selectedLangs[l.id]) {
              match = true;
            }
          });
          client.matchesLanguage = match;
        });
      }
    }
  };
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app="app">
  <div sample-directive>
    Languages:
    <div ng-repeat="lang in langs">
      <label>
        <input type="checkbox" ng-model="selectedLangs[lang.id]" name="languages_group" ng-change="filterByLanguage()">{{lang.name}}
      </label>
    </div>
    <br>Clients:
    <div ng-repeat="client in clientes" ng-show="client.matchesLanguage">{{client.name}} {{client.surname}}
    </div>
  </div>
</div>

Note the "filter" (now a function on the directive scope) is now only run when the ng-change handler is fired on a language selection; also instead of a separate selectedLanguages variable this just adds a 'matchesLanguage' field to each client for ng-show to use.

WHEN NOTHING IS SELECTED

For the exception requested in comments -- show all the clients if none of the languages are selected -- you could add another loop:

    scope.noLanguagesSelected = true;
    angular.forEach(Object.keys(scope.selectedLangs), function(k) {
      if (scope.selectedLangs[k]) {
        scope.noLanguagesSelected = false;
      }
    });

and then alter your ng-show to show the client if either that specific language, or no language at all is selected (this is probably better than just artificially setting client.matchesLanguage on everything in that case:)

ng-show="client.matchesLanguage || noLanguagesSelected"

as shown in the following:

var app = angular.module("app", []);

app.directive('sampleDirective', function() {
  return {
    restrict: 'A',
    link: function(scope) {
      scope.langs = [
        {'id': 1, 'name': 'english'}, 
        {'id': 2, 'name': 'spanish'},
        {'id': 3, 'name': 'german'}, 
        {'id': 4, 'name': 'japanese'}
      ];

      scope.clientes = [{
        'id': '3',
        'name': 'Susana',
        'surname': 'Rodríguez Torrón',
        'languages': [
          {'id': 1, 'name': 'english'}, 
          {'id': 2, 'name': 'spanish'}
        ]
      }, {
        'id': '4',
        'name': 'Pablo',
        'surname': 'Campos Pérez',
        'languages': [
          {'id': 3, 'name': 'german'}, 
          {'id': 4, 'name': 'japanese'}
        ]
      }];

      scope.selectedLangs = {};
      scope.noLanguagesSelected = true;

      scope.filterByLanguage = function() {
        angular.forEach(scope.clientes, function(client) {
          var match = false;
          angular.forEach(client.languages, function(l) {
            if (scope.selectedLangs[l.id]) {
              match = true;
            }
          });
          client.matchesLanguage = match;
        });
        
        /* 
        special case: if no checkboxes checked, pretend they all match.
        (In real life you'd probably wnat to represent this differently 
        in the UI rather than just setting matchesLanguage=true).
        We can't just check to see if selectedLangs == {}, because if user
        checked and then unchecked a box, selectedLangs will contain {id: false}.
        So we have to actually check each key for truthiness:
        */
        scope.noLanguagesSelected = true; // assume true until proved otherwise:
        angular.forEach(Object.keys(scope.selectedLangs), function(k) {
          if (scope.selectedLangs[k]) {
            scope.noLanguagesSelected = false; // proved otherwise.
          }
        });
      }
    }
  };
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app="app">
  <div sample-directive>
    Languages:
    <div ng-repeat="lang in langs">
      <label>
        <input type="checkbox" ng-model="selectedLangs[lang.id]" name="languages_group" ng-change="filterByLanguage()">{{lang.name}}
      </label>
    </div>
    <br>Clients:
    <div ng-repeat="client in clientes" ng-show="client.matchesLanguage || noLanguagesSelected">{{client.name}} {{client.surname}}
    </div>
  </div>
</div>

Answer

I would show all clients with css's "display:none" and a additional class with the language, so, then you can suscribe to the filter event:

filterEl.addEventListener("onchange",filter);

function filter(ev){
    // first hide all clients
    document.getElementsByClassName("clients").forEach(function(){
        this.style.display = "none";
    });

    // then show all clients with XXX language
    var checkbox = ev.target;
    document.getElementsByClassName(checkbox.id).forEach(function(){
        this.style.display = "block";
    });
}

CLIENT

<div class="client-wrapper LANGUAGE" ng-repeat="client in clients | orderBy:order_field:order_type | filter:query">
... all the data of every client showed here ...
</div>

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.