4

Background: I’m working on an angular app that displays a list of articles. The list can be modified by various settings. One such setting is the sources of these articles. Think of a source as a news agency: an article originates from a particular source:

enter image description here

So when a user clicks on the "Sources" link, a dropdown menu should appear which contains a list of the sources. The user can select any combination of these sources. There are also a "Select all" and "Clear all" buttons to either select all the sources or deselect them all:

enter image description here

Problem: So every time a user selects or deselects a source, an http request should be sent to the server, and the list of the articles should be updated.

My problem is, I am not sure how to call the function that will send an http request (in the code snippets below it's called updateArticleList()).

1) If I bind the function to ng-click and set it on the label tag:

<ul>
  <li ng-repeat="source in sources">
    <label ng-click="updateArticleList()">
      <input type="checkbox" ng-model="source.selected">
      {{source.title}}
    </label>
  </li>
</ul>

then a click on the label triggers the function twice (once for label, and, apparently, once for input). Not good.

2) If I bind the function to ng-change on the input tag:

<ul>
  <li ng-repeat="source in sources">
    <label>
      <input type="checkbox" ng-model="source.selected"
       ng-change="updateArticleList()">
      {{source.title}}
    </label>
  </li>
</ul>

then once I click on "Select all" or "Clear" buttons, this will change the state of all the checkboxes and send a lot of http requests. Not good either.

Right now, I am trying to solve this using setTimeout to filter out a burst of calls to the function to one, like so (example for calling the function through ng-click):

    var requestAllowed = true;

    var debounceRequests = function(){
      requestAllowed = false;
      setTimeout(function(){
        requestAllowed = true; 
      }, 5);
    };

    scope.updateArticleList = function(){
      if (requestAllowed === true){
        // prevent the second call to the function from ng-click
        debounceRequests();
        // also, give time for the input to register ng-click on the label
        setTimeout(function(){
             // finally, send an http request
             getArticles();
        }, 5);
      }
    };

But this looks dirty.

So, my question is, what would be a good way to make http requests in this situation?

Preferably, without using extra js libraries.

==================

UPDATED:

Here's the function that is triggered by "Select All":

    scope.selectAllSources = function(){
      scope.sources.forEach(function(source){
        source.selected = true;
      });
      scope.updateArticleList();
    };
azangru
  • 2,644
  • 5
  • 34
  • 55
  • 1
    Good question: well-presented objective and problem – New Dev Feb 18 '15 at 15:46
  • @NewDev is right, can you post your selectAll code? – Mathew Berg Feb 18 '15 at 15:49
  • @azangru, do you need to submit only the changed sources, or just resubmit all the selected ones (even if only 1 additional source was selected)? – New Dev Feb 18 '15 at 15:55
  • @NewDev: That is something I am still not quite sure about. At the moment, I am resubmitting all the list of the sources, indicating which of them are selected (`selected=true`) and which are not. Must be a waste of bandwidth :-( – azangru Feb 18 '15 at 15:58

3 Answers3

2

You should use ng-change.

ng-change only fires when the input is changing the model - not the other way around. Your selectAll should change the model. I'm guessing that you are doing the "select all" differently.

$scope.selectAll = function(){
  for (var i = 0; i < $scope.sources.length; i++) {
    $scope.sources[i].selected = true; // this does not fire `ng-change`
  }

  $scope.updateArticleList();
}

EDIT: Based on the OP's comment about submitting all the sources on every change, the following is a more complete conceptual example of how this could be achieved:

<li ng-repeat="source in sources">
   <label>
      <input type="checkbox" ng-model="source.selected" ng-change="updateArticleList()">
      {{source.title}}
   </label>
</li>
<button ng-click="selectAll()">select all</button>
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • That's interesting. I guess I haven't investigated the ng-change option that closely. I'll go and check whether it's giving me the problem that I tought it will give me or whether it was all my imagination. – azangru Feb 18 '15 at 15:56
  • Hey, you are right. Looks like I've been worrying too much, and doing something like `$scope.sources[i].selected = true;` in the directive will not trigger `ng-change`. That's awesome! Let me re-write my directive and see if anything breaks :-) – azangru Feb 18 '15 at 16:15
  • Yay! Works like a charm. – azangru Feb 18 '15 at 16:26
  • @azangru, yes, this is "by design". `ng-change` was meant to update the controller of the change. The controller doesn't need an additional trigger if it itself changes the model values. – New Dev Feb 18 '15 at 17:07
1

I liked your first solution better and after exploring around I remembered a similar question: Angular.js ng-click events on labels are firing twice

So a solution you can do is to check the event element and make sure the tag is right.

    $scope.updateArticleList = function(event){
        if(event.toElement.tagName == 'LABEL'){
            //run code
        }
    };

HTML

<label ng-click="updateArticleList($event)">

JSFiddle: http://jsfiddle.net/4p48q63j/

Community
  • 1
  • 1
Mathew Berg
  • 28,625
  • 11
  • 69
  • 90
0
<ul>
  <li ng-repeat="source in sources">
    <label>
      <input type="checkbox" ng-model="source.selected"
       ng-change="updateArticleList(source.selected)">
      {{source.title}}
    </label>
  </li>
</ul>

Controller; for individual selection....

$scope.updateArticleList=function(checked)
{
    if(checked==true)
    {
      //Call to service
    }
}

for selecteAll(button/Link) there are multiples ways to go with....

    $scope.selectAll = function(){
    angular.forEach($scope.sources,function(source,$index){
       // model value will be true for all sources....
       //Call to service
    });
    };
micronyks
  • 54,797
  • 15
  • 112
  • 146