Select2: Prevent select2:open when clearing selection

Created on 6 May 2015  ยท  78Comments  ยท  Source: select2/select2

Prevent catching the click (and disallow opening of the select or triggering the select2::open) when the clear indicator is pressed to clear the selection when allowClear is true.

This behavior worked fine in 3.5.2 - not sure if there is a reason for this.

Use case: When allowClear is true and one clicks the selection clear indicator -- the value gets cleared but it also triggers the dropdown to open again when it is not necessary.

4.x documentation good first issue stale

Most helpful comment

I can confirm this issue.

I think this issue should be considered a bug, since when a user wants to clear the select, he does not immediately want to select another value. And if he does, he would simply click on the element anywhere but the clear button.

All 78 comments

I did manage to implement this functionality by trapping the select2:opening event and aborting the open and also using the select2:unselect to cleanup/reset vars set at open if needed.

I can confirm this issue.

I think this issue should be considered a bug, since when a user wants to clear the select, he does not immediately want to select another value. And if he does, he would simply click on the element anywhere but the clear button.

@kartik-v Could you please explain what you do exactly? I tried cancelling the opening event using e.preventDefault(). This works, but I do get a JavaScript error TypeError: args is undefined.

My code as I now have it:

  $field.on 'select2:unselecting', ->
      $(this).data('unselecting', true)
    $field.on 'select2:opening', (e) ->
      if $(this).data('unselecting')
        $(this).removeData('unselecting')
        e.preventDefault()

@kevin-brown What is the reason for the new behaviour? Is it possible to restore the old behaviour, potentially as an option?

This works, but I do get a JavaScript error TypeError: args is undefined.

Known issue, tracking at https://github.com/select2/select2/issues/3431.

What is the reason for the new behaviour?

Based on the commit (https://github.com/select2/select2/commit/ffed37013d3c8af77c3777d71d45abc10649dbb4), there wasn't any major reason for it.

Is it possible to restore the old behaviour, potentially as an option?

I'm open to the idea of it, though it would have to be done in a way such that it didn't break backwards compatibility.

Could you please explain what you do exactly? I tried cancelling the opening event using e.preventDefault(). This works, but I do get a JavaScript error TypeError: args is undefined.

Yes its similar to yours (probably yours is better) - and more of an hack - which you can check on my Yii2 Select2 widget demo pages - but I am also facing the args undefined message as you mentioned (probably only when I have change events on the input) and related to #3431.

At other times I am getting an error cannot set property prevented of undefined in select2.full.js Line 5161.

// initialize a variable like this (better if this variable is unique for each Select2 input)
var isClearClicked = false;
// trap these events
$field.on('select2:opening', function(e) {
    if (window['isClearClicked']) {
        e.preventDefault();
        window['isClearClicked'] = false;
    }
}).on('select2:unselect', function(e) {
     window['isClearClicked'] = true;
});

I modified the hack to the following code combining all the thoughts in this thread (with some additions) and it seems to work without errors (if you can forgive the flicker of open and close of the dropdown) - so still a dirty hack... till we get this solved...

var $el = $('#select-id');
$el.on('select2:opening', function(e) {
    if ($el.data('unselecting')) {    
        $el.removeData('unselecting');
        setTimeout(function() {
            $el.select2('close');
        }, 1);
    }
}).on('select2:unselecting', function(e) {
    $el.data('unselecting', true);
});

Another side effect of clear - #3452 - we probably need additional events.

The following works for me:

            $("select").on("select2:unselecting", function (e) {
                $(this).select2("val", "");
                e.preventDefault();
            });

Is it possible to restore the old behaviour, potentially as an option?
I'm open to the idea of it, though it would have to be done in a way such that it didn't break backwards compatibility.

I agree with others, this is odd default behaviour - it doesn't match the default behaviour of the standard multiselect HTML element on Windows (Chrome, FF and standard Office and Windows UI tested) or MAC, and in our focus group test of our new UI, it confused and frustrated users (as they had to do an additional click to access options below the select2 element after they had cleared all the selected items from the multiselect)

Further to this is is a change in behaviour from Select2 v3, so yet another incompatibility - with no evidence supported reason for the change :(

I would like to see the default behaviour changed to match v3, or at the very least a option for it.

@NovaFocus solution doesn't appear to work when the select is configured for multiselect

Here's another hack for those that can't stand the quick flicker that shows up in kartik's solution (full credit to him for his solution being a springboard to this solution):

var select2Obj = $('select');
select2Obj.select2();

/* The following mess is a result of Select2's behavior of triggering the
 * opening event twice upon unselecting an item. */
select2Obj.on('select2:unselecting', function(e) {
    $(this).data('unselecting1', true);
    $(this).data('unselecting2', true);
});
select2Obj.on('select2:open', function(e) {
    var unselecting1 = $(this).data('unselecting1');
    var unselecting2 = $(this).data('unselecting2');

    if(unselecting1 || unselecting2) {
        $(this).select2('close');

        if(unselecting1) {
            $(this).data('unselecting1', false);
        } else {
            $(this).data('unselecting2', false);
        }
    }
});
// End of mess.

:+1: for making it to not open the dropdown.

Maybe an option like "autoOpenOnClear" could solve the issue. Or people could trigger the opening theirselves with the unselecting event?

@SectorNine50 your solution works, but disables the normal opening of the dropdown for one click after unselecting from the dropdown list, so i came up with this, witch i think works better because it prevents the open event from firing in the first place:

var select2Obj = $('select');
select2Obj.select2();

select2Obj.on('select2:unselecting', function(e) {
    $(this).on('select2:opening', function(e) {
        e.preventDefault();
    });

});
select2Obj.on('select2:unselect', function(e) {
     var sel = $(this);
     setTimeout(function() {
       sel.off('select2:opening');
     }, 1);
});

Obviously, replace " sel.off('select2:opening'); " with your own code for taht event if you're using it
.
Enjoy :)

@Hlsgs Odd, it doesn't have that behavior on my end... Both of the unselecting "variables" should be unset immediately after deselecting an object. Does one remain set to true after deselecting on your end?

That said, what you came up with is a good solution as well!

@SectorNine50 unfortunately, i don't know how to watch for variables that are not defined straight in js. btw, how would one watch for unselecting1 and unselecting2 in your code?

regardless, i think this behaviour is because unselecting by clicking the "x" in teh selection actually reopens the dropdown but unselecting from the dropdown does not. so after unselecting we're left with one more round of preventing teh dropdown from opening.

i've dug a little deeper here, and decided to fix select2.js as per here 6be96cfaa175448ef70b978648de7d6cb5822e89 and use preventdefault() as TypeError: args is undefined is a select2 issue and this is going to be in future releases anyway.
after that, i wanted to go with @kreintjes 's solution as it seemed teh most to the point to me, but that didn't work i think because the open event gets fired twice(witch you accounted for by using two variables, correct?)
so i made a hybrid of your and his solution like this

$(this).on('select2:unselecting', function(e) {
    $(this).data('unselecting1', true);
    $(this).data('unselecting2', true);
});
$(this).on('select2:opening', function(e) {
    var unselecting1 = $(this).data('unselecting1');
    var unselecting2 = $(this).data('unselecting2');

    if(unselecting1 || unselecting2) {

        e.preventDefault();

        if(unselecting1) {
            $(this).data('unselecting1', false);
        } else {
            $(this).data('unselecting2', false);
        }
    }
});

This produced exactly the same results as your vanilla solution, and after deselecting from the dropdown the opening of it gets blocked for a round.

In the end i went back to my solution, as it doesn't rely on setting variable and, by hooking into the :unselect event to restore opening functionality it disregards wether the dropdown was actually opened or not. And the timeout accounts for the opening event firing more than once(or not).

Considering all this, i should be fine with this solution even if in future updates open fires only once and/or the developer prevents the opening of the dropdown in the original code. Correct?

EDIT:
Actually it seems that the timeout i added has nothing to do with the open event firing more that once as i fixed that as per 2acf9f31d3a281defb4bd5c2810a0ee8b46ea140 and it seems the code doesnt work without it anyway.
I'm starting to despise my feeble understanding of js :)
Please can someone explain why this works:

    $(this).on('select2:unselecting', function(e) {
        $(this).on('select2:opening', function(event) {
            event.preventDefault();
        });
    });
    $(this).on('select2:unselect', function(e) {
         var sel = $(this);
         setTimeout(function() {
           sel.off('select2:opening');
         }, 100);
    });

and this doesn't:

    $(this).on('select2:unselecting', function(e) {
        $(this).on('select2:opening', function(event) {
            event.preventDefault();
        });
    });
    $(this).on('select2:unselect', function(e) {
         var sel = $(this);
         sel.off('select2:opening');
    });

unfortunately, i don't know how to watch for variables that are not defined straight in js. btw, how would one watch for unselecting1 and unselecting2 in your code?

Within your debugger, you should be able to dig into the parent variable $(this) and find the data object, which should contain the unselected variables.

Considering all this, i should be fine with this solution even if in future updates open fires only once and/or the developer prevents the opening of the dropdown in the original code. Correct?

If future versions only fire open once or not at all, you'll have to change the code, since one (or both) variable(s) will still remain set, meaning that the next attempt to open will be blocked.

I'm not entirely sure what your patch changes, honestly. I'll have to dig a little more into the core code to get an understanding.

+1 for this...great component anyway

                $(this).select2("val", "");
                e.preventDefault();

Works great, thanks

Still no solution?

The planned solution is to add a select2:clear event that will contain the originalEvent, so we can just call stopPropagation() on that to prevent the opening. That would make the solution similar to https://github.com/select2/select2/issues/3209#issuecomment-149663474.

@SectorNine50 and others... with rel v4.0.1... I have modified the hack slightly to the following. It now seemingly works without any flicker or the above issues and with a leaner code.

var $el = $('#select-id');
$el.on('select2:unselecting', function(e) {
    $el.data('unselecting', true);
}).on('select2:open', function(e) { // note the open event is important
    if ($el.data('unselecting')) {    
        $el.removeData('unselecting'); // you need to unset this before close
        $el.select2('close');
    }
});

@kartik-v Very nice, that's much more concise!

@kartik-v Thanks, that works.

+1 On separating unselect and open.

@kartik-v Cheers for the snippet!

If anyone is still have issues with this here is a simple clear solution that is working for me:

$("Selector").on("select2:unselecting", function (e) {
    $(this).select2("val", "");
    e.preventDefault();
});

@nkamenar that solution has side-effects however.. With that change the "val","" call will clear out ALL selections in a multi-select if you are simply wanting to unselect a single option.

@kartik-v your solution seems the best.. though you don't need the "$el" var as you can use $(this).

$('.select2').select2().on("select2:unselecting", function (e) {
    $(this).data('unselecting', true);
  }).on('select2:open', function(e) {
    if ($(this).data('unselecting')) {
      $(this).select2('close').removeData('unselecting');
    }
  });

@nkamenar's solution now prevents select2 from clearing it's selected value in 4.0.3.
@urkle/@kartik-v - this solution is working for me.

@nkamenar This is not working anymore on 4.0.3

@urkle That works.

Hi All, for what it's worth I thought I'd add yet another snippet to this thread! It's a variant of others that have been used. This snippet avoids any blinking of the select opening relying on preventDefault to stop the open event.

$el.on('select2:opening', (e) => {
    if ($el.data('unselecting')) {    
        $el.removeData('unselecting');
        e.preventDefault();
    }
}).on('select2:unselecting', (e) => {
    $el.data('unselecting', true);
});

I confirm that @urkle option works like a charm.

@Hlsgs: the code in your last snippet from October 28th doesn't work because you're removing the select2:opening event handler on the wrong event. select2:unselect fires immediately after select2:unselecting, before select2:opening fires. I do like your approach the best, though. Directly manipulating the event handlers seems much cleaner than setting a temp data property, in my eyes.

I took that approach, set it on the correct event handlers, and extended it to fix a similar issue for multi-selects (the dropdown closes if it's open and you remove an item). Here's the code:

var $element = $('select')

// Select2 4.0 incorrectly toggles the dropdown when clearing and removing options.
// Fix this by canceling the select2:opening/closing events that occur immediately after a select2:unselect event.
$element.on('select2:unselect', function() {
  function cancelAndRemove(event) {
    event.preventDefault()
    removeEvents()
  }
  function removeEvents() {
    $element.off('select2:opening', cancelAndRemove)
    $element.off('select2:closing', cancelAndRemove)
  }
  $element.on('select2:opening', cancelAndRemove)
  $element.on('select2:closing', cancelAndRemove)
  setTimeout(removeEvents, 0)
})

@ScottTrenda Hmm, I think I'm going to use your code aswell as i dislike the closing of the multiselect dropdown while unselecting. Thanks!

So how would you correct the code i had so as to work without requiering a timeout?

By adding your handlers to the select2:unselect and select2:opening events instead of the select2:unselecting and select2:unselect events, respectively.

I updated the code above slightly, to remove the event handlers in a timeout as well as on the events. The immediate (incorrect) opening/closing events don't fire when unselect happens via keyboard backspace, and the previous code was preventing the next (correct) opening/closing event afterward in that case.

If someone is using angular 1.x, here's how I wrapped @kartik-v's answer in a directive:

;(function(ng) {
  "use strict";

  ng.module('myApp')
    .directive('select2AllowClearFix', [function() {
        return function(scope, element, attrs) {
          element.on('select2:unselecting', function(e) {
              element.data('unselecting', true);
          }).on('select2:open', function(e) {
              if (element.data('unselecting')) {
                  element.removeData('unselecting');
                  element.select2('close');
              }
          });
        }
    }]);
}(angular));

And then use it like:

<select multiple
        class="my-select"
        select2-allow-clear-fix
        style="width: 100%"
        ng-model="model.info"
        ng-options="info as info.name for info in InfoCtrl.infos track by info.id"></select>

Why not just remove
this.trigger('toggle', {});
in line 65 of /src/js/select2/selection/allowClear.js

and then add
evt.stopPropagation();
to line 43 of /src/js/select2/selection/multiple.js?

My current fix is changing these two lines from select2.min.js and it works

After digging into code it seems that setting a disabled option to true also prevents dropdown from opening.

this.inputElement.on('select2:unselect', () => {
        this.inputElement.data('select2').options.set('disabled', true);
    setTimeout(() => {
        this.inputElement.data('select2').options.set('disabled', false);
    }, 0);
});

@asdsteven that's not a good solution, because you should never touch the dist files.

The solution from @creage seems best so far.

Most other solutions break this scenario:

  1. select an option
  2. open dropdown and pick the same option again (unselects it)
  3. try to open dropdown (prevented, because the previous step did not fire extraneous open events, so this one was prevented instead)
  4. try to open dropdown (works again)

Solution from @creage breaks main scenario for me:

  • Open dropdown and pick an option different than the current one.
  • The dropdown doesn't close.

I'm using 4.0.3.

UPDATE:
Sorry, my mistake. I had another unrelated issue making it malfunction. @creage solution works ok for me.

Yes this solution suggested works without needing to tamper with the open event (slightly modified solution from @creage). Phew...

$('#select-id').on('select2:unselecting', function() {
    var opts = $(this).data('select2').options;
    opts.set('disabled', true);
    setTimeout(function() {
        opts.set('disabled', false);
    }, 1);
});

Hey, @kartik-v, I found a fix by @phlax : https://github.com/translate/pootle/pull/5590

But I figured out a much shorter and less error-prone solution ๐Ÿ˜‰

$('#select-id').on('select2:unselecting', function() {
    $(this).one('select2:opening', function(ev) { ev.preventDefault(); });
});

I'm using Select2 version 4.0.4

It's worth mentioning that, as of 4.0.6, unselect and clear will be separate events. See #5058. Can someone confirm whether or not this resolves the problem (or makes it worse? ๐Ÿ˜จ )

@alexweissman What do you mean with "whether or not this resolves the problem" โ€“ do you mean my code or the pull request you are mentioning? I just tested version 4.0.6-rc.1 โ€“ my code still works. But if you are asking whether the pull request resolves the issue we are trying to overcome, then no, in the version 4.0.6-rc.1 this issue still exists.

I can tell you how to fix the issue, if you want to:
Just remove this.trigger('toggle', {}); in https://github.com/select2/select2/blob/4.0.6-rc.1/dist/js/select2.js#L1893 to prevent opening when clearing (single mode). Why is it there anyway?! ๐Ÿ˜€
And add evt.stopPropagation(); after line 1690 https://github.com/select2/select2/blob/4.0.6-rc.1/dist/js/select2.js#L1690 to prevent opening when removing (multiple mode).

Until it gets fixed... I found out a little bit more effective way to overcome (to hack) the issue. I found out that you can access the original event when unselecting (in multiple mode), so you don't have to bind a single-use event, you can just stopPropagation() the original event. But for clearing (in single mode) you still have to bind single-use event, because it calls this.trigger('toggle', {}); no matter what you do (for now).

$('#select-id').on('select2:unselecting', function(ev) {
    if (ev.params.args.originalEvent) {
        // When unselecting (in multiple mode)
        ev.params.args.originalEvent.stopPropagation();
    } else {
        // When clearing (in single mode)
        $(this).one('select2:opening', function(ev) { ev.preventDefault(); });
    }
});

I think it should work for all 4.0.x versions. I have tested it for versions 4.0.4 and 4.0.5.

@taai it sounds like you are much more intimately familiar with the issue than I am. Would you consider submitting a pull request?

Also, if you look at @kevin-brown's last comment on this:

The planned solution is to add a select2:clear event that will contain the originalEvent, so we can just call stopPropagation() on that to prevent the opening. That would make the solution similar to #3209 (comment).

So, now that we have a select2:clear event, we should probably implement it this way.

The alternative is to introduce another boolean config option, but I really hate introducing more boolean config options. The more we add, the more unintended interaction effects tend to crop up.

@alexweissman I don't think this should be prevented by binding an event โ€“ for Select2 users (developers) it is an overkill just to get rid of weird behavior of Select2. If opening after clearing/unselecting is really needed, then we should implement a boolean config option... But personally I think that the behavior (opening after clearing/unselecting) is weird. By the way, in "multiple" mode this behavior was just a side effect โ€“ the click just propagated to other click bindings โ€“ and was not intended in the first place. If it should open, in code it should be executed specifically, like that this.trigger('toggle', {}); which I removed in my pull request. ๐Ÿ˜‰

Not sure if this is related, but should the clear button disable the V on the dropdowns? Is this a config issue on our side maybe or is it a flaw in select2?

image

@piotrekkmt No, it should be like that. It means you can either clear/unselect by clicking on that X or select another value by clicking on that V. Otherways, to select another value, you would have to click more than once โ€“ starting with X, then clicking V... Yeah, probably the behavior of all this should be configurable, but the way it is right now makes sense to me. ๐Ÿ˜‰

My approach!

$('select').on("select2:unselecting", function (e) { 
    // make sure we are on the list and not within input box
    if (e.params._type === 'unselecting') {
        $(this).val('').trigger('change');
        e.preventDefault();
    }
});

@Apezdr I think that ends method leaves the