0

Consider the following HTML snippet: This is basically a nested group of checkboxes, using unordered list markup to create a general grouping. The code is being generated dynamically so I have complete control over the markup as needed.

A jsfiddle representing the problem below is at: http://jsfiddle.net/bwh4rj3h/

I am attempting to create jQuery that performs the following actions:

  1. Clicking Select All toggles the state of all the checkboxes in the snippet.
  2. Clicking on a checkbox deeper in the matrix will toggle all "sub-checkboxes". e.g. clicking on the Group1 checkbox will toggle the state of all the checkboxes in the construct underneath the group.
  3. If any checkboxes are unchecked in given group, then the group checkbox must also be deselected (While technically a tri-state checkbox representing a "partial" fill would be nice, that's beyond the scope of my question). e.g. Say item 1-1, 1-2, and 1-3 are all checked. Then group 1 should be checked, and in this example the "Select All" should also be checked. However if I deselect item 1-2, now group 1 is not "complete" and so should be unchecked, as should the "Select All".

OK, so I have some jQuery code that works for the markup below. It works fine. But it's ugly. And I hate ugly and feel like there's a flaw in my logic, selectors, and/or HTML markup that's causing things to be this ugly and that there IS A BETTER WAY.

Would love to get some feedback as to what that better way is. In my personal case, I don't have to worry about more than 2 levels deep, but a generic N-levels deep functionality shouldn't be too hard. In fact, if it weren't for the 3rd condition where I'm checking the state of "children" to see if everything is checked/unchecked, it would've been relatively straightforward. The fact that I'm doing a 3 time loop (because of order of evaluation) makes me sad.

Your assistance is appreciated in advance. :)

You'll notice that I am using a :disabled check. In my final project, I am setting some checkboxes to be disabled and as such they are "immune" from my 3 requirements listed above. Since the markup doesn't include any disabled items, this should not affect the outcome.

<ul>
   <li>
       <input type="checkbox" id="chkAll" checked/><label for="chkAll">Select All</label>
   </li>
   <ul>
       <li>
           <input type="checkbox" id="chkGroup1" checked/><label for="chkGroup1">Group 1</label>
       </li>
       <ul>
           <li>
               <input type="checkbox" id="chkGp1-Item1" checked/><label for="chkGp1-Item1">Item 1</label>
           </li>
           <li>
               <input type="checkbox" id="chkGp1-Item2" checked/><label for="chkGp1-Item2">Item 2</label>
           </li>
       </ul>
    </ul>
</ul>

The jQuery:

  $("input[type=checkbox]:not(:disabled)").on("change", function () {
                $(this).parent().next("ul").find("li input[type=checkbox]:not(:disabled)").prop("checked", $(this).prop("checked"));

                // Looping 3 times to cover all sublevels.  THERE HAS GOT TO BE A BETTER WAY THAN THIS.

                for (var i = 0; i < 3; i++) {

                    $("input[type=checkbox]:not(:disabled)").each(function() {

                        var uncheckedkidcount = $(this).parent().next("ul").find("li input[type=checkbox]:not(:disabled):not(:checked)").length;
                        var kidcount = $(this).parent().next("ul").find("li input[type=checkbox]:not(:disabled)").length;

                        if (kidcount > 0) {
                            $(this).prop("checked", uncheckedkidcount == 0);
                        }
                    });
                }
            }); 
Huangism
  • 16,278
  • 7
  • 48
  • 74

2 Answers2

2

I have forked and updated your JSFiddle with a different solution. I made a few changes to the markup that I think would make it easier to deal with, but the jQuery is still lengthy. Hopefully this helps.

Here is the updated JSFiddle. I have commented the JS to explain how it all works.

For your markup - I don't think it's good practice to nest a <ul> directly in another <ul> without wrapping it in an <li> (see this stack overflow question for more info. Additionally, if you have full control over the DOM structure, I would add a few class names to make the jQuery more manageable:

<ul>
    <li>
        <input type="checkbox" id="chkAll" checked/>
        <label for="chkAll">Select All</label>
    </li>
    <li class="has-nested-list">
        <ul class="nested">
            <li>
                <input type="checkbox" id="chkGroup1" checked/>
                <label for="chkGroup1">Group 1</label>
            </li>
            <li class="has-nested-list">
                <ul class="nested">
                    <li>
                        <input type="checkbox" id="chkGp1-Item1" checked/>
                        <label for="chkGp1-Item1">Item 1</label>
                    </li>
                    <li>
                        <input type="checkbox" id="chkGp1-Item2" checked/>
                        <label for="chkGp1-Item2">Item 2</label>
                    </li>
                </ul>
            </li>
        </ul>
    </li>
</ul>

To make this work without having multiple bullets when there is a nested list, we can add this simple CSS rule

.has-nested-list {
    list-style-type: none;
}

The jQuery is all inside the fiddle, and I won't post it to help make this more brief.

Hope this helps.

Cheers

Community
  • 1
  • 1
grammar
  • 871
  • 10
  • 22
  • This is very nicely done. You're right, the DOM sniffing in some parts are pretty nasty, and I hear you in regards to the way I'm directly nesting a
      tag within a
    • tag. I was wondering about that at the time, so thanks for the link. I'm going to leave this answer up for a couple more days (I didn't see when you answered it, so I apologize... I thought I would get an email notification) and in lieu of something superior, will accept this. Thanks so much for taking the time, grammar. I just spent an hour answering another question for someone else... Stack Overflow is a good thing!
    – Vincent Polite Sep 15 '14 at 15:28
  • Glad I could help. Stackoverflow is the best thing to happen to the internet since Google :) – grammar Sep 15 '14 at 15:46
2

I've stumbled upon your question while trying to implement the same thing.

Here is how I solved it: https://codepen.io/nexx/pen/VyLLPM.

It supports multiple levels of nested checkboxes and follows the logic mentioned in the question. Hope it might help someone struggling with the same thing. As a beginner, I would love to hear feedback :)

 <ul class="categories-list">
  <li class="category"><input type="checkbox"><label>Parent category</label>
    <a class='collapse-category' href=''>expand</a>
    <ul class="subcategories">
      <li class="category"><input type="checkbox"><label>Child category </label></li>
      <li class="category"><input type="checkbox"><label>Child category </label></li>
      <li class="category"><input type="checkbox"><label>Child category </label></li>
      <li class="category"><input type="checkbox"><label>Child category </label></li>
      <li class="category"><input type="checkbox">
      <label>Child category with children</label>
        <a class='collapse-category' href=''>expand</a>
        <ul class="subcategories">
          <li class="category"><input type="checkbox"><label>Child category </label></li>
          <li class="category"><input type="checkbox"><label>Child category </label></li>
          <li class="category"><input type="checkbox"><label>Child category </label></li>
          <li class="category"><input type="checkbox"><label>Child category </label></li>
          <li class="category"><input type="checkbox">
          <label>Child category with children</label>
            <a class='collapse-category' href=''>expand</a>
            <ul class="subcategories">
              <li class="category"><input type="checkbox"><label>Child category </label></li>
              <li class="category"><input type="checkbox"><label>Child category </label></li>
              <li class="category"><input type="checkbox"><label>Child category </label></li>
              <li class="category"><input type="checkbox"><label>Child category </label></li>
              <li class="category"><input type="checkbox"><label>Child category </label></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul> 

jQuery

$.fn.checkboxParent = function() {
//get parent checkbox element
  var checkboxParent = $(this)
  .parent("li")
  .parent("ul")
  .parent("li")
  .find('> input[type="checkbox"]');
  return checkboxParent;
};

$.fn.checkboxChildren = function() {
  //get checkbox children
  var checkboxChildren = $(this)
  .parent("li")
  .find(' > .subcategories > li > input[type="checkbox"]');
  return checkboxChildren;
};


$.fn.cascadeUp = function() {
  var checkboxParent = $(this).checkboxParent();
  if ($(this).prop("checked")) {
    if (checkboxParent.length) {
      //check if all children of the parent are selected - if yes, select the parent
      //these will be the siblings of the element which we clicked on
      var children = $(checkboxParent).checkboxChildren();
      var booleanChildren = $.map(children, function(child, i) {
        return $(child).prop("checked");
      });
      //check if all children are checked
      function and(a, b) {
        return a && b;
      }
      var allChecked = booleanChildren.reduce(and, true);
      if (allChecked) {
        $(checkboxParent).prop("checked", true);
        $(checkboxParent).cascadeUp();
      }
    }
  } else {
    if (checkboxParent.length) {
      //if parent is checked, becomes unchecked
      $(checkboxParent).prop("checked", false);
      $(checkboxParent).cascadeUp();
    }
  }
};
$.fn.cascadeDown = function() {
  var checkboxChildren = $(this).checkboxChildren();
  if ($(this).prop("checked")) {
    if (checkboxChildren.length) {
      //all children are checked
      checkboxChildren.prop("checked", true);
      checkboxChildren.each(function(index) {
        $(this).cascadeDown();
      });
    }
  } else {
    if (checkboxChildren.length) {
      //children become unchecked
      checkboxChildren.prop("checked", false);
      checkboxChildren.each(function(index) {
        $(this).cascadeDown();
      });
    }
  }
};

$("input[type=checkbox]:not(:disabled)").on("change", function() {
  $(this).cascadeUp();
  $(this).cascadeDown();
});

$(".category a").on("click", function(e) {
  e.preventDefault();
  $(this)
    .parent()
    .find("> .subcategories")
    .slideToggle(function() {
    if ($(this).is(":visible")) $(this).css("display", "flex");
  });
});

css (sass):

.categories-list
  margin: 40px 0px
  padding: 0px 10px
  width: 300px
  .category
    font-size: 14px
    display: flex
    flex-direction: column
    position: relative
    padding: 3px 0px 3px 22px
    border-bottom: 1px solid #f5f5f5
    font-weight: 300
    display: flex
    label
      width: 100%
    a
      color: #95939a
      position: absolute
      right: 0px
      z-index: 1000
    input[type="checkbox"]
      margin: 0px 10px 0px 0px
      position: absolute
      left: 0px
      top: 7px
    .subcategories
      margin-left: 0px
      display: none
      padding: 5px
      flex-direction: column
      .category
        padding-left: 22px
        flex-direction: column
        &:last-child
          border-bottom: none
nexx
  • 39
  • 5