incutonez
incutonez

Reputation: 3331

AngularJS: Grandchild checkbox select from Parent checkbox

I'm trying to create a hierarchy of checkboxes in AngularJS. I can create a parent checkbox that will automagically check the child checkbox, and if I manually check the child checkbox, it will update its child's (the grandchild of the parent) checkbox. However, if I click the parent checkbox, it will not update the grandchild's checkbox.

Before I go adding in JavaScript to make sure the parent checkbox updates the grandchild's checkbox, I'd just like to know if there's an easier way. Here's my code and a jsFiddle for your enjoyment.

<div ng-app="">
    <strong>Parent</strong> (will check all Child boxes):
    <input type="checkbox" ng-model="parent" />
    <br/>
    <strong>Child 1</strong>:
    <input type="checkbox" ng-checked="parent" />
    <br />
    <strong>Child 2</strong>:
    <input type="checkbox" ng-checked="parent" />
    <br />
    <strong>Child 3</strong> (will check all Grandchild boxes):
    <input type="checkbox" ng-checked="parent" ng-model="child" />
    <br />
    <strong>Grandchild 1</strong>:
    <input type="checkbox" ng-checked="child" />
    <br />
    <strong>Grandchild 2</strong>:
    <input type="checkbox" ng-checked="child" />
    <br />
    <strong>Grandchild 3</strong>:
    <input type="checkbox" ng-checked="child" />
</div>

Any help would be appreciated!

Also, could someone with 1500 rep help me out by creating the tag "angularjs-ng-checked"? That'd be awesome!

Upvotes: 1

Views: 3866

Answers (2)

incutonez
incutonez

Reputation: 3331

I've been doing some fiddling (ha ha) around, and I came up with a solution that seems to be pretty generic... meaning, you can have as many nested checkboxes as your heart desires. I'm sure there are some strange practices that I'm following, and there may be areas where performance could be increased--this is definitely looping through lots of objects--so feel free to criticize it! I'd love to hear any feedback.

The code can be found at this jsFiddle. I'll also post it here.

The HTML

<!DOCTYPE html>
<html>
  <head>
    <script src="../js/angularjs106.js"></script>
    <script src="angular_checkbox.js"></script>
    <link rel="stylesheet" href="angular_checkbox.css" />
  </head>
  <body ng-app>
    <div ng-controller="Ctrl">
      <ul>
        <li>
          <input type="checkbox" value="allChecks.id" ng-model="allChecks.isChecked" ng-change="checkChange(allChecks)">
          <label>{{allChecks.id}}</label>
        </li>
        <li ng-repeat="child in allChecks.children">
          <input type="checkbox" ng-model="child.isChecked" ng-change="checkChange(child)">
          <label>{{child.id}}</label>
          <ul>
            <li ng-repeat="grandchild in child.children">
              <input type="checkbox" ng-model="grandchild.isChecked" ng-change="checkChange(grandchild)">
              <label>{{grandchild.id}}</label>
              <ul>
                <li ng-repeat="grandgrandchild in grandchild.children">
                  <input type="checkbox" ng-model="grandgrandchild.isChecked" ng-change="checkChange(grandgrandchild)">
                  <label>{{grandgrandchild.id}}</label>
                </li>
              </ul>
            </li>
          </ul>
        </li>
      </ul>
    </div>
  </body>  
</html>

The JavaScript

function Ctrl($scope) { 
  function Node(id, isChecked, parent) {
    this.id = id;
    this.isChecked = isChecked;
    this.parent = parent;
    this.children = [];
  }

  var parent = new Node('ALL', true, "");
  var child1 = new Node('Child 1', true, parent);
  var gchild1 = new Node('Grandchild 1', true, child1);
  var gchild2 = new Node('Grandchild 2', true, child1);
  var gchild3 = new Node('Grandchild 3', true, child1);
  child1.children.push(gchild1, gchild2, gchild3);
  var child2 = new Node('Child 2', true, parent);
  var child3 = new Node('Child 3', true, parent);
  var child4 = new Node('Child 4', true, parent);
  var gchild4 = new Node('Grandchild 4', true, child4);
  var gchild5 = new Node('Grandchild 5', true, child4);
  var gchild6 = new Node('Grandchild 6', true, child4);
  var ggchild1 = new Node('Grandgrandchild 1', true, gchild6);
  var ggchild2 = new Node('Grandgrandchild 2', true, gchild6);
  var ggchild3 = new Node('Grandgrandchild 3', true, gchild6);
  gchild6.children.push(ggchild1, ggchild2, ggchild3);
  child4.children.push(gchild4, gchild5, gchild6);
  var child5 = new Node('Child 5', true, parent);
  parent.children.push(child1, child2, child3, child4, child5);
  $scope.allChecks = parent;

  function parentCheckChange(item) {
    for (var i in item.children) {
      item.children[i].isChecked = item.isChecked;
      if (item.children[i].children) {
        parentCheckChange(item.children[i]);
      }
    }
  }

  function childCheckChange(parent) {
    var allChecks = true;
    for (var i in parent.children) {
      if (!parent.children[i].isChecked) {
        allChecks = false;
        break;
      }
    }
    if (allChecks) {
      parent.isChecked = true;
    }
    else {
      parent.isChecked = false;
    }
    if (parent.parent) {
      childCheckChange(parent.parent);
    }
  }

  $scope.checkChange = function(item) {
    // We're handling the ALL checkbox
    if (item.id === $scope.allChecks.id) {
      parentCheckChange(item);
    }
    // We're handling the toggling of all of the children here
    else {
      if (item.children) {
        parentCheckChange(item);
      }
      childCheckChange(item.parent);
    }
  };
}

The tiny CSS

ul {
  list-style: none;
}

Anyway, I wanted to figure out a way of finding the parent in an object. Turns out, the native JavaScript object doesn't hold a reference to a child's parent object, so I decided to create a class called Node. When I instantiate the Node, I can set the parent to the object that contains the child. Once I do this, recursion becomes quite easy.

parentCheckChange is called when we realize we have a parent. childCheckChange is called when the child's check changes (duh!), but it's used to keep an eye on all of the parent's children... if all of them are checked, then we need to make sure the parent is checked, and because this parent has been checked, we need to check if its parent should be checked (recursively).

The code may be a bit confusing, so please ask questions if you're confused!

Upvotes: 0

Langdon
Langdon

Reputation: 20073

Unfortunately there is no simpler approach.

From Misko via github:

... we think what you are trying to do does not make sense. ng-checked creates a one-way data-binding but ng-model creates two-way databinding. In essence you have two models connected to the same input element, and i think that that is just wrong. I think ng-model and ng-checked should be used exclusively, ie one or the other but not together.

Outside of using a directive or synchronizing the checks yourself in a controller, there are not a whole lot of options.

Upvotes: 3

Related Questions