3

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.

9
  • Google angular customer filter. Commented Jan 18, 2016 at 17:16
  • I have googled it before asking, but can't find the solution. Commented Jan 18, 2016 at 17:33
  • What did you try? Maybe consider posting the code that you wrote for the filter function. Commented Jan 18, 2016 at 18:17
  • Just a tip: using filters is not performant, because it has to re-evaluate those filters every digest. Instead, generate the list of records matching all the filters as a separate scope property only whenever the user changes one of them, and bind to that instead. Bonus tip: don't forget to use track by in your ng-repeat expression to allow reusing DOM for the rows that don't change. Commented Jan 19, 2016 at 0:56
  • I have posted the code, but it is incomplete and don't runs. Only for showing you. Commented Jan 19, 2016 at 8:18

2 Answers 2

2
+50

(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>

Sign up to request clarification or add additional context in comments.

7 Comments

Incredible answer Daniel, I was having understanding issues with a lot of things you talked here. I am pleased, thanks a lot. Only one question (I haven't studied your code yet), you logic is inverse, I mean, It is supposed to be a filter, all the clients must be visible, except when you check a checkbox, then only clients WITH THE SELECTION will be visible. That means that if I check german and english, the visible client must have english and german. This is for me the most difficult of the case.
If I'm understanding you, you're not looking for the inverse of the logic I have; instead the same logic (if any are checkboxes checked, show clients who do have that language) but with one additional exception (if no checkboxes are checked, show all clients)? If that's correct I can add that to the answer
Or wait. Now that I've added that example, I'm realizing you might have meant something else -- in order for a client to match, they need to have all of their languages checked, not just at least one of their languages (as i coded it)? i.e. do you want that if Jane has english and german, but I've only checked german, Jane shouldn't show up?
Sorry for not answering before, my work took all my time. I want to see all the people when the checkboxes are unchecked. When I check english, I want to see all the people who speaks that language, don't matter if they speak another language, if they speak english, they are showed. For example, Jane speaks english and german, Tob speaks spanish and english, and Mai speaks japanese and spanish. If no check checked we will see 3 people. If check english we will see Jane and Tob.
Awesome. OK then -- the third snippet in my answer behaves as you describe (it's identical to the 2nd snippet, but with the extra bit to handle the "noLanguagesSeleted" case.)
|
0

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>

1 Comment

It is never a good idea to modify the DOM directly in Angular; all your setting of this.style.display will get wiped out in the next $digest.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.