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.

Gajus
  • 69,002
  • 70
  • 275
  • 438
Programador Adagal
  • 780
  • 14
  • 39
  • Google angular customer filter. – Shaohao Jan 18 '16 at 17:16
  • I have googled it before asking, but can't find the solution. – Programador Adagal Jan 18 '16 at 17:33
  • What did you try? Maybe consider posting the code that you wrote for the filter function. – Shaohao Jan 18 '16 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. – GregL Jan 19 '16 at 00:56
  • I have posted the code, but it is incomplete and don't runs. Only for showing you. – Programador Adagal Jan 19 '16 at 08:18
  • is your issue to get languages which are checked in checkbox-block or issue is to filter clients which contains selected language,,, or you have both issues :D – Nino Mirza Mušić Jan 19 '16 at 09:42
  • The issue is to filter the clients which contains the selected languages. Only that. – Programador Adagal Jan 19 '16 at 10:36
  • While u can do it in angular (client side) you really shouldn't, set up a service with lazy loading and serve the results from back end – Yerken Jan 21 '16 at 15:50
  • @Yerken that's a... bold statement – Daniel Beck Jan 21 '16 at 16:23

2 Answers2

2

(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>
Daniel Beck
  • 20,653
  • 5
  • 38
  • 53
  • 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. – Programador Adagal Jan 22 '16 at 09:15
  • 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 – Daniel Beck Jan 22 '16 at 13:51
  • 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? – Daniel Beck Jan 22 '16 at 20:17
  • 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. – Programador Adagal Jan 26 '16 at 08:16
  • 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.) – Daniel Beck Jan 26 '16 at 12:58
  • Thanks a lot for all your work, that helped me to understand the filters system and to improve my code. I am so grateful with you ^^ – Programador Adagal Jan 26 '16 at 16:58
  • No trouble at all. Cheers – Daniel Beck Jan 26 '16 at 16:59
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>
PRDeving
  • 679
  • 3
  • 11
  • 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. – Daniel Beck Jan 21 '16 at 17:35