3

I am trying to add a bootstrap dropdown inside another clickable parent. It's kind of tricky I guess. Here is the Codepen I created with the issue recreated.

Codepen with the issue recreated

Expected behavior: On clicking the dropdown, the dropdown will fire, so will by clicking the other button (optional if too tricky) and none will trigger a click on the parent.

    <div class="card-body parent" style="min-height: 300px;">
      <!-- ORIGINAL DROPPER BUTTON -->
      <div class="dropdown original-dropper mb-5">
        <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
          Dropdown button
        </button>
        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
          <a class="dropdown-item" href="#">Action</a>
          <a class="dropdown-item" href="#">Another action</a>
        </div>
      </div>

      <!-- OTHER DROPPER BUTTON -->
      <button class="btn btn-danger other-dropper mt-5 d-inline">Other Button</button>
    </div>

JS:

 $('.parent').on('click', function(e) {
  $(this).css('backgroundColor', 'darkorange');
});

$('.other-dropper').on('click', function(e) {
  e.stopPropagation();
  $('.original-dropper .dropdown-toggle').dropdown();
  console.log('clicked');
});
Tomáš Zato
  • 50,171
  • 52
  • 268
  • 778
RP McMurphy
  • 704
  • 8
  • 30
  • Ugh. Codepen forces login to edit and save. Anyway, what about click on the dropdown itself, should it also not propagate to parent? Sometimes it's better to filter propagated events at the parent than block them from the child nodes. – Tomáš Zato Sep 05 '19 at 15:56
  • You don't need to call `dropdown()`, that will create dropdown menu, you need to add classes to open dropdown, The easiest way is open dev tools open dropdown and see what changing (show class are added to two elements) so you need to add same classes with jQuery. Best is to use `.toggleClass('show')` – jcubic Sep 05 '19 at 15:57
  • @jcubic Wouldn't that interfere with the dropdowns internal state, eg. causing it not to close properly? – Tomáš Zato Sep 05 '19 at 16:12
  • @TomášZato It seems you're right, check [How to open Bootstrap dropdown programmatically](https://stackoverflow.com/a/35760042/387194) – jcubic Sep 05 '19 at 17:19
  • @jcubic Yep, that QA helped me with the answer, although there seems to be a lot of uncertainty and the answers vary. – Tomáš Zato Sep 05 '19 at 17:22
  • @TomášZato I think that `.dropdown('toggle')` it the right way, it's part of Boostrap API. – jcubic Sep 05 '19 at 17:50

1 Answers1

1

First, it's better to filter an event at the target instead of calling stopPropagation. If you stop propagation of event, it prevents any other global listeners from capturing them. For example, other dropdowns will not close if click event propagation was stopped.

So instead I introduced this neat filter method, that allows you to check if the source of event or it's parents has a class:

/**
 * Checks if any of parent nodes of elm has a class name
 * @param {HTMLElement} elm the starting node
 * @param {string} className
 * @param {HTMLElement} stopAtElm if this parent is reached, search stops
**/
function treeHasClass(elm, className, stopAtElm) {
  while(elm != null && elm != stopAtElm) {
    if(elm.classList.contains(className)) {
      return true;
    }
    elm = elm.parentNode;
  }
  return false;
}

Unfortunately you still need stopPropagation for the button, else the click event would close the drop-down immediately.

Second, you want to call .dropdown("toggle"):

$('.original-dropper .dropdown-toggle').dropdown("toggle");

All together, the javascript is:

$('.parent').on('click', function(e) {
  // Do not trigger if target elm or it's child has `no-orange` class
  if(!treeHasClass(e.target, "no-orange", this)) {
    $(this).css('backgroundColor', 'darkorange');
  }
});

$('.other-dropper').on('click', function(e) {
  e.stopPropagation();
  e.preventDefault();
  $('.original-dropper .dropdown-toggle').dropdown("toggle");
  console.log('clicked');
});

And you want to put no-orange to elements that shouldn't change background color:

<button class="no-orange btn btn-danger other-dropper mt-5 d-inline">Other Button</button>

All together here: https://codepen.io/MXXIV/pen/rNBpepd

Tomáš Zato
  • 50,171
  • 52
  • 268
  • 778
  • Elegantly done! One follow up question, is e.preventDefault(); necessary in the .other-dropper click function? And here in the Bootstrap documentation it's missing the 'toggle' part of the function call - https://getbootstrap.com/docs/4.0/components/dropdowns/#via-javascript. Or there is something I missed? Thanks a bunch. – RP McMurphy Sep 05 '19 at 17:22
  • @RPMcMurphy I'm not a bootsrap expert. All my google searches have shown that there's a bit of chaos regarding how to do that. The `preventDefault` is sadly needed - otherwise the propagated click event immediately causes the menu to close again, as it counts as click outside the dropdown. – Tomáš Zato Sep 05 '19 at 17:24
  • If you really needed to not use the prevent default, you could instead use `setTimeout(()=>{/*toggle menu here*/})`, but I think that's even uglier. – Tomáš Zato Sep 05 '19 at 17:25
  • @Zato I edited the pen and clicking on the original dropdown button is not triggering the parent background to go orange though clicking on child should do that as I am not filtering anything. You think Bootstrap already took care of what the helper function you created does? Could you take a look- https://codepen.io/rpmcmurphy/pen/OJLzMrY?editors=0010 I am stopping propagation only on the other button, not the original dropdown button. Just trying to understand if the helper function becomes redundant. – RP McMurphy Sep 05 '19 at 17:31
  • @RPMcMurphy The helper function is for the dropdown itself and all it's items. In the other button, the `stopPropagation` does the job, but that's only a side effect. Real reason for `stopPropagation` is to prevent the dropdown from being closed by the click event. – Tomáš Zato Sep 05 '19 at 17:35