0
\$\begingroup\$

Just thought I'd share this and see what kind of improvements could be made that I'm not aware of and taking advantage of.

The situation is this: I have a list of items on a page that have a button that opens a menu. I need a click away event as well as close others when a new menu is opened. Also some of the menu actions will replace items in the list which means I have new dynamic menu buttons on the page.

To do this efficiently (as far as I can tell) you need to use event delegation and it ends up looking like this:

$(document).delegate( '.button', 'click', function(e){
    var $button = $(this);
    var $menu = $button.closest('.menu');
    e.stopPropagation();

    // Close others
    $('.button').each(function(){ 
      $(this).closest('.menu').removeClass('open'); 
    });

    // Toggle target open/close
    if( $menu.hasClass('open') ){
        $menu.removeClass('open');
    }
    else{
        $menu.addClass('open');
    }
    $(document).on('click', function(e){
        var $target = $(e.target);
        if( !$target.is( $( $menu.find('button') ) ) ){
            if( $menu.hasClass('open') ){
                $menu.removeClass('open');
            }
        }
    });
});

Maybe there's a better way?

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

I believe there is no better way. Regardless, we can shorten the code a little. Also note that .delegate() has been superceded by .on() since jQuery 1.7, so I replaced your delegate with on.

$(document).on( 'click', '.button', function(e){
    var $button = $(this);
    var $menu = $button.closest('.menu');
    e.stopPropagation();

    // Close others-assumed you want all menus closed
    $('.menu').not($menu).removeClass('open'); 

    // Toggle target open/close
    $menu.toggleClass('open');

    $(document).on('click', function(e){
        //This function will only run when e.target isn't a button
        //Your $target.is($menu.find('button')) will always be false

        if( $menu.hasClass('open') ){
            $menu.removeClass('open');
        }
    });
});

If this does not do the same thing, you really need to share your html structure so we can get a better idea of your mark-up.

Edit To work around the fact that the document click event is assigned every time you click on a button, you could do something like the following:

$(document).ready(function() {
   var $menu;
   $(document).on( 'click', '.button', function(e){
      var $button = $(this);
      $menu = $button.closest('.menu');
      e.stopPropagation();
      $('.menu').not($menu).removeClass('open'); 
      $menu.toggleClass('open');
   });
   $(document).on('click', function(e){
      if($menu && $menu.hasClass('open') ){
            $menu.removeClass('open');
        }
   });
)}
\$\endgroup\$
4
  • \$\begingroup\$ I forgot to add that your first call to $(document) should be using a more precise selector. From the event performance section of the doc for .on() "For best performance, attach delegated events at a document location as close as possible to the target elements. Avoid excessive use of document or document.body for delegated events on large documents." So instead of using $(document) you should use $(sharedParentSelector). \$\endgroup\$
    – Daniel
    Commented Nov 5, 2013 at 18:28
  • 2
    \$\begingroup\$ This code (and the original) will keep adding document click event handlers every time you click a .button... \$\endgroup\$
    – Flambino
    Commented Nov 6, 2013 at 1:34
  • \$\begingroup\$ You're exactly right Flambino, which is part of what I was looking to fix. My first inclination would be to namespace the event on document and somehow add the event when the button is clicked and remove it when the document is clicked. Not sure how that would work here though. \$\endgroup\$
    – Tomas
    Commented Nov 7, 2013 at 17:18
  • \$\begingroup\$ I'd suggest moving the second event declaration to outside the first. Wrapping both in a function like $(function() {. Then move $menu to be scoped within this outer function and use it in both inner functions. This way the $(document).on('click', function(e){ will only trigger once. I'll update my answer to show what I mean. \$\endgroup\$
    – Daniel
    Commented Nov 7, 2013 at 17:40
1
\$\begingroup\$

Slight variation of @DanielCook's answer, just the 'on click outside, close menu' part :

$(document).on('click', function(e){
  // ensure this is only applied if the element is not contained in a menu
  if ($(e.target).is(':not(.menu, .menu *)')){
      // no need to check if element has class .open, 
      // removeClass does nothing if it is not the case
      $('.menu').removeClass('open'); 
  }
});
\$\endgroup\$
1
  • \$\begingroup\$ Missed a ) in the if statement. Otherwise this is perfect. \$\endgroup\$
    – Tomas
    Commented Nov 7, 2013 at 17:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.