Monday, September 8, 2014

Multi-Option Picker using Bootstrap Dropdowns with Checkboxes

I wanted check boxes in my Bootstrap dropdown menu so the user could pick from several different options and see which options were already selected. The menu's default behavior is to hide once the user clicks on it. This meant I needed to cancel the click event to prevent that behavior. Unfortunately, if the user clicked a checkbox, it wouldn't show as checked since the click was being canceled (try clicking the checkbox versus the anchor and notice how it doesn't toggle):

See the Pen Bootstrap Dropdown with Checkboxes: Can't Check by bseth99 (@bseth99) on CodePen.



For reference, this is the markup for the dropdown with the checkboxes:


<ul class="dropdown-menu">
<li>
<a href="#" class="small" data-value="option1" tabIndex="-1">
<input type="checkbox"/>&nbsp;Option 1
</a>
</li>
<li>
<a href="#" class="small" data-value="option2" tabIndex="-1">
<input type="checkbox"/>&nbsp;Option 2
</a>
</li>
<li>
<a href="#" class="small" data-value="option3" tabIndex="-1">
<input type="checkbox"/>&nbsp;Option 3
</a>
</li>
<li>
<a href="#" class="small" data-value="option4" tabIndex="-1">
<input type="checkbox"/>&nbsp;Option 4
</a>
</li>
<li>
<a href="#" class="small" data-value="option5" tabIndex="-1">
<input type="checkbox"/>&nbsp;Option 5
</a>
</li>
<li>
<a href="#" class="small" data-value="option6" tabIndex="-1">
<input type="checkbox"/>&nbsp;Option 6
</a>
</li>
</ul>


My goal is to use one event handler for both the anchor and the check box. On each click, toggle the option, and ensure the check box reflects this choice. When you click the anchor, the check box needs to be set. However, if you click the check box, it wouldn't need to be set, but to be consistent and use one handler, I just set it anyways (I saw no reason to add any more sophisticated element detection to the code):

var options = [];

$( '.dropdown-menu a' ).on( 'click', function( event ) {

   var $target = $( event.currentTarget ),
       val = $target.attr( 'data-value' ),
       $inp = $target.find( 'input' ),
       idx;

   if ( ( idx = options.indexOf( val ) ) > -1 ) {
      options.splice( idx, 1 );
      $inp.prop( 'checked', false );
   } else {
      options.push( val );
      $inp.prop( 'checked', true );
   }

   $target.blur();

   return false;
});
I want the return false in there to prevent the dropdown from closing and, secondarily, to ensure the href="#" doesn't affect my router. I could remove the href from the anchor, but then I'd lose the hand cursor on hover. I could style it have the cursor, but I'd still be left with the auto-closing menu which I don't want since the user may want to pick other options. That leaves me with the conundrum of dealing with a direct click on the box which also is a click on the anchor which needs to be canceled but canceling it also doesn't check the box properly. I can think of several ways around it - all adding way more code than makes sense for such a seemingly simple problem. After some toiling, I finally found a very easy work-around. It occurred to me that I just needed to check the box after all the events finished running. That means I needed to be outside the current call stack to check the box. Using setTimeout with a zero delay does just that so I wrapped that bit of code in a setTimeout and - viola - working checkboxes:


   setTimeout( function() { $inp.prop( 'checked', true ) }, 0);



And here's the whole working example:

See the Pen Bootstrap Dropdown with Checkboxes: Working by bseth99 (@bseth99) on CodePen.



And with that, the Bootstrap drop down become just that much more useful.