nine9ths
nine9ths

Reputation: 795

AngularJS checkbox ng-checked not rendering if preventDefault used

I'm dynamically building a set of checkboxes. Clicking any of the checked boxes should uncheck the first (index wise) checked box. Clicking any of the unchecked boxes should check the last unchecked box.

I'm building the checkboxes using ng-repeat like this:

<input
  type="checkbox"
  ng-checked="values[$index]"
  ng-repeat="n in values track by $index" 
  ng-click="click($event,$index)" />

And my controller looks like this:

.controller("myCtrl", function ($scope) {

$scope.values = [true,true,true,true];

$scope.click = function (event,n) {
  event.preventDefault();
  if ($scope.values[n] === true) {
    $scope.values[$scope.values.indexOf(true)] = false;
  } else {
    $scope.values[$scope.values.lastIndexOf(false)] = true;
  }
}

Here's a codepen of it all together http://codepen.io/anon/pen/rVLewJ

The problem that I'm running into is that using preventDefault seems to prevent ng-checked from updating the rendering of the box you click on (the others boxes re-render correctly). This causes the display to become out of sync with $scope.values.

Likewise, removing preventDefault doesn't prevent the box you're clicking on from changing its rendering, but (I believe because of ng-repeat's conservative re-rendering) ng-checked doesn't fire so it also gets out of sync.

I'm not using ng-changed because I'm specifically trying to prevent the checkboxes from changing if you're clicking on the "wrong" one. Regardless, I've tried using it instead of ng-clicked and it didn't fix anything. I've tried using ng-model instead of ng-checked, but that seemed to prevent $scope.values from changing at all. Using $scope.$apply(), didn't help. Some of the things I've read have lead me to think I may need to use $watch but I'm teaching myself Angular with this project so I'm not sure exactly how to apply that.


Update

Nagasimha Iyengar provided a working solution here which I've simplified thusly

<input
       type="checkbox"
       ng-model="values[$index]"
       ng-repeat="n in values track by $index" 
       ng-change="click($index)" />

Controller:

.controller("myCtrl", function ($scope) {

$scope.values = [true,true,true,true];

$scope.click = function (n) {
  if ($scope.values[n] === true) {//clicking on unchecked box
    $scope.values[n] = false;
    $scope.values[$scope.values.lastIndexOf(false)] = true;
  } else {//clicking on checked box
    $scope.values[n] = true;
    $scope.values[$scope.values.indexOf(true)] = false;
  }
}

Codepen: http://codepen.io/anon/pen/zGBKxr

This solution is based on allowing the click event to happen, then undoing it before proceeding with the custom logic. It's certainly a simple solution but feels somewhat improper. Is it the most correct way of solving this problem?

Another small update. I dropped in ngTouch to try and make the app feel a bit quicker on mobile. At least in iOS Safari 8, ngTouch broke this solution. Still works fine on desktop, but the overridden ngClick prevents this solution form working. If you switch back to the original proposed logic it fixes iOS, but of course doesn't work on the desktop. I feel like this confirms my suspicion that the solution was not the correct one.

Upvotes: 2

Views: 2876

Answers (2)

Nagasimha Iyengar
Nagasimha Iyengar

Reputation: 461

Here it is - the logic was reversed. And I used $apply with a timeout. Modified codepen: http://codepen.io/nagasimhai/pen/VLjjPe

angular
.module("myApp", [])
    .controller("myCtrl", function ($scope) {

    $scope.values = [true,true,true,true];

    $scope.click = function (event,n) {
      //event.preventDefault();
      //console.log("b", n, $scope.values,$scope.values.indexOf(true), $scope.values.lastIndexOf(false));
      if ($scope.values[n] === true) {//clicking on unchecked box
        $scope.values[n] = false;
        $scope.values[$scope.values.lastIndexOf(false)] = true;
        //console.log("11");
      } else {//clicking on checked box
        $scope.values[n] = true;
        $scope.values[$scope.values.indexOf(true)] = false;
        //console.log('22');

      }
      //console.log("a",n, $scope.values);
      setTimeout(function () {
        $scope.$apply(function () {

        });
    }, 2000);
    }
});

Upvotes: 1

Garrett Kadillak
Garrett Kadillak

Reputation: 1060

Your answer was a bit confusing but I believe I'm on the correct path. The problem. What you want to use is event.stopPropagation() to stop the event from propagating. In other words, you only want the event to apply to the specific element. If you use event.preventDefault() it cancels the event if cancelable, without stopping further propagation of the event.

angular
.module("myApp", [])
.controller("myCtrl", function ($scope) {

$scope.values = [true,true,true,true];

$scope.click = function (event,n) {
  event.stopPropagation();
  if ($scope.values[n] === true) {
    $scope.values[$scope.values.indexOf(true)] = false;
    $scope.values[n] = false;
  } else {
    $scope.values[$scope.values.lastIndexOf(false)] = true;
  }
}
});

Try the code out at the codepen http://codepen.io/anon/pen/bdepPW

Upvotes: 0

Related Questions