Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

In multi-select, selections do not appear in the order in which they were selected #3106

Closed
matthew-dean opened this issue Mar 4, 2015 · 79 comments

Comments

@matthew-dean
Copy link

On this page: https://select2.github.io/examples.html

...All the tags appended to the end show up in an arbitrary place in the Select2 box. There seems to be no rhyme or reason where they will appear, neither alphabetical nor sorting by list matching. It just randomly sorts each time. This seems to be the reverse of issue #2283, so I'm not sure how to actually force the tags to append.

@kevin-brown
Copy link
Member

I'm not quite sure what you mean here.

For me, the tags are appended to the end of the selection when they are created. In the dropdown, they appear at the bottom after they are created, and at the top when they do not yet exist.

@matthew-dean
Copy link
Author

Hawaii selected, then Alaska.

screen shot 2015-03-04 at 1 05 14 pm
screen shot 2015-03-04 at 1 05 25 pm
screen shot 2015-03-04 at 1 05 33 pm

@matthew-dean
Copy link
Author

Order these were added: bar, foo, green, blue, red

screen shot 2015-03-04 at 1 08 47 pm

@kevin-brown
Copy link
Member

Ah, now I understand.

The order that the items are displayed is the same order that they will be sent to the server. This is also the same order that they are displayed in the dropdown (usually).

@matthew-dean
Copy link
Author

That's..... huh? Why would you want tags to appear in a different place than the cursor position? The dropdown position is not relevant to the order in which I add tags. I could see them rearranged in a certain order on page reload, but while typing / selecting is jarring. If you add emails to an email, your email program doesn't swap the tags based on alphabetical position. It represents a linear, comma-separated list that just happens to be tokenized for easy removal of items, or to drag/drop items, or match to a list. I don't get this behavior. OR: if this is intuitive to others, can linear append be an option?

@kevin-brown
Copy link
Member

Usually when working with tagging, there isn't an existing data set or it's pulled from a remote data source when there is (which is unordered on our end). In both of these cases, new additions are always appended to the end.

You can test this by always creating new tags and seeing the order that they appear once they are selected. It should be in the order that they were created, and they should be sent to the server in that same order. If this isn't the case, then yes there is definitely an issue.

The difficulty comes when there are new additions to the end and then an existing one is selected, as it most likely isn't at the end as well. There used to be complaints (which made sense) because what was displayed did not match what was being sent to the server, as the ordering was off.

@matthew-dean
Copy link
Author

If you tag a post in Wordpress, you can add arbitrary members, but you can also select from a list of tags. Either way, selections are added linearly as that's the way they were selected chronologically. Tags always indicate the order in which a user selected them (or typed them). This is done because a backspace then deletes them from right-to-left reverse chronologically. Or you could say it's done because of left-to-right reading conventions corresponding to order of operations.

If the complaint is that the ordering to the server doesn't match the selection order, then I would change the ordering sent to the server. OR allow a switch to the current behavior of partial sorting of tags within the list by two sets of criteria (matches and non-matches, the first sorted by list order, the second by chronological selection order), but I would expect that to be the outlier in terms of expected behavior, since a two-method partial-list-sort algorithm seems hard to intuit. In fact, at first I thought the list was completely randomized each time, until I figured out that the list was invisibly divided between matches and non-matches, with each one sorting independently by different criteria.

So.... select2 is a great component. I'd really like to use it more. Can another option be added?

@kevin-brown
Copy link
Member

So, ultimately the issue here is that Select2 is trying to normalize how the results are displayed to match how the data is sent to the server. This is because that previously wasn't consistent (or documented) and it had some really strange side effects ("what you see is not what you get") and inconsistencies ("only when working with a <select> though!").

If the complaint is that the ordering to the server doesn't match the selection order, then I would change the ordering sent to the server.

Great idea! I just took a shot at doing this by automatically re-inserting the most recently selected element to the end of the <select> and that worked pretty well.

http://jsbin.com/cizehajema/1/edit

image

Of course, this means that now the results list changes every time, because the <option> is being moved. But this can be worked around by just moving all selected tags to the top of the results, which might appear more natural.

http://jsbin.com/cizehajema/2/edit

image

@matthew-dean
Copy link
Author

I think a few things are being conflated. For example, this:

Of course, this means that now the results list changes every time, because the is being moved.

The drop-down order shouldn't really have anything to do with tag order. The tags (tokens) are the order of selection. The drop down is really the fixed dataset, usually alphabetical or whatever the sort method was. If the drop down is being populated BY arbitrarily-added tags, then it probably makes some sense, and in that case the drop-down should mimic selection order. (Although I can't think of a case where having arbitrary added values in the drop-down makes sense anyway. It seems like the dropdown is only useful when it shows imported data, not user-added data.)

But, if, say, the dataset is an imported list of U.S. states., then of course the selected tags should never re-order the drop-down, because it's a fixed list of available options.

So, I think it's important to have three distinct datasets:

  1. The drop-down list data
  2. Selections
  3. Data to the server

It sounds like 1 and 3 need to be synced to avoid confusion, but number 2 needn't be.

Just my $0.02.

@ppawel
Copy link

ppawel commented May 24, 2015

Yeah it looks like that's another select2 oddity here...

So, I think it's important to have three distinct datasets (...)

+1. That's definitely the expected behavior.

@alejcas
Copy link

alejcas commented Jun 30, 2015

Also when using multiple= true (no tags), the order that I would like to have is not the order in which the options are ordered but the order in which the user is selecting the options. Any ideas about this?

@Intrepidd
Copy link

+1 on @janscas's concern.

It's particularly problematic when adding a tag by mistake then pressing backspace to remove it, if the tag is inserted in another position, you'll start deleting the wrong tag

@xelax90
Copy link

xelax90 commented Mar 14, 2016

I just ran into the issue that on server-side I need the order in which the items were selected. I wrote a simple script that just reorders the options inside the select every time, an item is selected:

$this.on('select2:select', function(e){
    var elm = e.params.data.element;
    $elm = jQuery(elm);
    $t = jQuery(this);
    $t.append($elm);
    $t.trigger('change.select2');
});

Here is also a JsFiddle.
This addresses all the issues in here and I think it would be a nice feature for select2.

@shashilo
Copy link

@kevin-brown Your code worked for me except the unselect. The unselected item was getting removed completely after you add/remove it multiple times.

`$("select").select2({
tags: true
});

// On select, place the selected item in order
$("select").on("select2:select", function (evt) {
    var element = evt.params.data.element;
    var $element = $(element);

    window.setTimeout(function () {  
      if ($("select").find(":selected").length > 1) {
        var $second = $("select").find(":selected").eq(-2);
        $element.detach();
        $second.after($element);
      } 
      else {
        $element.detach();
        $("select").prepend($element);
      }

      $("select").trigger("change");
    }, 1);

});

// on unselect, put the selected item last
$("select").on("select2:unselect", function (evt) {
    var element = evt.params.data.element;
    $("select").append(element);
});`

@BackuPs
Copy link

BackuPs commented May 2, 2016

Hi

How to make this work as i already have a $element with the select2 object

if($element.data("order")==true){
    $element.select2();
        //view the current data.
    console.log($element.select2('data'));
}

Now how do i hook into this with above code example?

Please assist. Thank you.

And another question. Will their be a update of the script where the multiselect select order is adjustable?

And how to load the correct order of the selected items on modification of the items? My list of options is created in php where the selected=selected is set based on the setting stored in the database.

this is how my code looks on init

But they were selected this way

b|infomation,p|document,s|building

<select id="_slideshow_category" class="themechosen select2-hidden-accessible" style="width:60%;height:auto" size="5" multiple="" name="_slideshow_category[]" .="" data-order="true" data-placeholder="Select Source.." tabindex="-1" aria-hidden="true">
<option value="g">Self Gallery</option>
<optgroup label="SlideShow Category">
<option value="s">(s) All</option>
<option selected="selected" value="s|building">(s) building</option>
<option value="s|homepage">(s) homepage</option>
<option value="s|videos">(s) video</option>
</optgroup>
<optgroup label="Blog Posts Category">
<option value="b">(b) All</option>
<option selected="selected" value="b|infomation">(b) Infomation</option>
<option value="b|news">(b) News</option>
</optgroup>
<optgroup label="Portfolio Category">
<option value="p">(p) All</option>
<option value="p|animals">(p) Animals</option>
<option selected="selected" value="p|document">(p) Document</option>
</optgroup>
</select>

What i want is to have the result screen to present the order of the items as they are selected once the data-order="true"

Best regards,
BackuPs

@piotr-cz
Copy link

piotr-cz commented Jun 3, 2016

I think I solved that by adding custom DataAdapter which:

  • adds a timestamp to option when selected
  • removes it when unselected
  • sorts options by timestamp when retrieved

Adapter inherits native methods which don't have specified API visibility (public/ protected) so there is no guarantee it will work in future versions. Tested on Select2 v4.0.2

See this gist

@aropixel
Copy link

Hi,

The Adapter works, but it's not possible to create tag anymore (it's only possible to choose an existing one). Would you have a solution for that ?

Thank you

@benjiwheeler
Copy link

benjiwheeler commented Jul 23, 2016

  1. agree with matthew-dean that this behavior is confusing and unexpected to the user.
  2. fwiw, i found this code fix effective, though it doesn't work for token separators:
    (found on Select2 - How to stop automatic sorting? angular-ui/angular-ui-OLDREPO#406 )
// with this element...
$("select").select2({
  tags: true,
  tokenSeparators: [',', ' '] // note: doesn't work for these! hitting comma or space will reorder tags
});
// apply tag order fix
$("select").on("select2:select", function (evt) {
  var element = evt.params.data.element;
  var $element = $(element);
  $element.detach();
  $(this).append($element);
  $(this).trigger("change");
});

@vol7ron
Copy link

vol7ron commented Aug 25, 2016

@benjiwheeler Unfortunately, that approach (also found here: http://stackoverflow.com/questions/31431197/select2-how-to-prevent-tags-sorting) does not maintain the order of the options list.

Select a value from the list and another; notice it preserves the selection order, but that also modifies the selection list. Clear them and try to select them via mouse again and notice they appear at the end of the options list (no longer in the original sort).

@vol7ron
Copy link

vol7ron commented Aug 25, 2016

The order that the items are displayed is the same order that they will be sent to the server. This is also the same order that they are displayed in the dropdown (usually).

@kevin-brown While this is great from a developer's standpoint, this is assuming that the select data is being sent to the server. In many cases, select options are used for UI to modify the data internal to the page and not even sent to the server.

For those cases, this is very confusing. Better UX is to display the data as it is selected and not make the user hunt through the selected options to verify that it was selected. If a developer needs to preserve that order when sending to the server, they will need to account for that in their AJAX or form submission event.

@Tagirijus
Copy link

@Oliboy50 :

  • I already tested every workaround here and no-one worked when using AJAX fr filling the data.
  • Doesn't select2 depend on jQuery?

@artjomsimon
Copy link

@Tagirijus unfortunately, not a single of the workarounds @Oliboy50 refers to works at the moment without side effects that render the select box unusable in one way or the other.

https://github.com/jshjohnson/Choices might be a solution for your use case. It's a dependency-free multi-select library. For my use cases it worked well and solves the order problem.

@Tagirijus
Copy link

Hey, @artjomsimon , thanks for the hint. I will have a look. (=

@ryan2johnson9
Copy link

ryan2johnson9 commented May 17, 2018

@artjomsimon Choices looks nice, good to be able to specify shouldSortItems: false without a workaround...
But I still had a problem, when I reloaded the saved form, the chosen options were reordered depending on the order of the collection, there was no obvious option to prevent that in the Choices.js doc . Neither is there one in Select2, but rather than add a whole new library, I stayed with this workaround, Select2, and manually ordering my collection when rendering the html, putting saved items in their present order at the top of the collection.

// NOTE - you also have to massage the options and put the chosen persisted options in the correct order at top of the collection
function preserveOrderOnSelect2Choice(e){
  var id = e.params.data.id;
  var option = $(e.target).children('[value='+id+']');
  option.detach();
  $(e.target).append(option).change();
}
po_select2s = $('select.select2-preserve-order').select2()
po_select2s.each(function(){
  $(this).on('select2:select',preserveOrderOnSelect2Choice);
});

@vol7ron
Copy link

vol7ron commented Aug 21, 2018

@Tagirijus I posted a solution earlier that works. I imagine it's not being adopted because it contains more lines of code, but it is also more robust than most of the solutions posted here. There was a race condition for some reason, which AJAX introduced, which I have since patched with a timeout.

You can view it here:
https://jsfiddle.net/bkc7qt36/292/

When wanting to retrieve the values in presented order it is important to use $select2.data('preserved-order'): ["c", "a", "b"] (and not $select2.val()). The demo shows what both resolve to; val is in list-order, preserved-order is in selection order (desired behavior).

@Tagirijus
Copy link

Thanks for the reply. I'll check it out, if I have some time.

@dejurin
Copy link

dejurin commented Sep 6, 2018

For remote data I was solved problem with help schang933 message #3106 (comment). Thank you.

@pedrofurtado
Copy link
Contributor

We don't have immediate plans to provide this. We are focused to fix some major UI bugs (that are majority of issues and PR's). But if you open a PR with unit tests, I will be glad to review and approve if everything is ok 👍

@sk-karthik
Copy link

sk-karthik commented Oct 8, 2018

I think I solved that by adding custom DataAdapter which:

  • adds a timestamp to option when selected
  • removes it when unselected
  • sorts options by timestamp when retrieved

Adapter inherits native methods which don't have specified API visibility (public/ protected) so there is no guarantee it will work in future versions. Tested on Select2 v4.0.2

See this gist

I'm facing the problem when using customAdapter,
image

were I clicked [13,22,11], I expected to display were last selected get into first like this [11,22,13],

if I removed customAdapter working fine.

@gwcms
Copy link

gwcms commented Jan 14, 2019

none of above provided sugestions worked for me, finally came to this solution:
`//to prevent auto ordering after select
$('#yourElementId').on("select2:select", function (evt) {
var element = evt.params.data.element;
var $element = $(element);
$element.detach();
$(this).append($element);
$(this).trigger("change");
});

//allow ordering
$.fn.select2.amd.require([
'select2/utils',
'select2/dropdown',
'select2/dropdown/attachBody'
], function (Utils, Dropdown, AttachBody) {
var select = $('#yourElementId');
var container = select.select2().data('select2').$container;

container.find('ul').sortable({
containment: 'parent',
update: function(event, ui) { 				
	$(ui.item[0]).parent().find('.select2-selection__choice').each(function(){ 
		var elm = Utils.__cache[$(this).data('select2Id')];
		var element = elm.data.element;
		var $element = $(element);
		$element.detach();
		select.append($element);
		select.trigger("change");
	})
	}
});	

})
`

@sarangtc
Copy link

sarangtc commented Aug 5, 2019

So, ultimately the issue here is that Select2 is trying to normalize how the results are displayed to match how the data is sent to the server. This is because that previously wasn't consistent (or documented) and it had some really strange side effects ("what you see is not what you get") and inconsistencies ("only when working with a <select> though!").

If the complaint is that the ordering to the server doesn't match the selection order, then I would change the ordering sent to the server.

Great idea! I just took a shot at doing this by automatically re-inserting the most recently selected element to the end of the <select> and that worked pretty well.

http://jsbin.com/cizehajema/1/edit

image

Of course, this means that now the results list changes every time, because the <option> is being moved. But this can be worked around by just moving all selected tags to the top of the results, which might appear more natural.

http://jsbin.com/cizehajema/2/edit

image

the function for reordering after deselect should be:
$("select").on("select2:unselect", function (evt) {
if ($("select").find(":selected").length > 0) {
var element = evt.params.data.element;
var $element = $(element);
$
("select").find(":selected").eq(-1).after($element);
}
});

@bci24
Copy link

bci24 commented Aug 19, 2019

@sarangtc

if you try to set a value to selected (ex: <option selected four>) you will see that when try to add a new value (ex: two) ... and you will see that value will disappear!

@ercanertan
Copy link

Select2 4.0.3
this works for me:

.on('select2:select', function(e){
      var id = e.params.data.id;
      var option = $(e.target).children('[value='+id+']');
      option.detach();
      $(e.target).append(option).change();
    });

I am using icon/image and ,it only worked with double quotes .

$(e.target).children('[value="'+id+'"]');

@jeffpowell
Copy link

The solution from @ercanertan works well (original solution from @2rba).

@vol7ron
Copy link

vol7ron commented Aug 17, 2020

These follow-up solutions are solving one problem or another, but not most of the problems all at once.

For anyone reading this (untested on newer version of Select), I still recommend tracking and applying the preserved order during selection and unselection as mentioned here: #3106 (comment)

It is important for the ordering in the dropdown to remain in tact, while the selection order (and sent to the server) reflects the order the items are selected/displayed. It is also important for this process to be reset and undone.

@kingIZZZY
Copy link

kingIZZZY commented Feb 23, 2021

+1 please add this feature

Meanwhile - workaround

This worked for me:

After much hacking around - this hack messes around with the Select2 display elements directly, namely the <li> elements in the salect2 display <ul>. So it does not change the <select>'s <option>s
Warning: Makes use of the internal undocumented _resultId property, I don't know if that means there's no guarantee this property will always be there 🤷‍♂️

$("select").on("select2:selecting select2:unselecting", function (evt) {
    var _id = evt.params.args.data._resultId.match(/select2-(\w+)-result/)[1];
    var ul = $(`ul#select2-${_id}-container`)
    var tmpord = ul.children('li').map((idx, elm) => CSS.escape(elm.title)).get()
    tmpord.push(CSS.escape(evt.params.args.data.text))
    ul.data('tmpord', tmpord)
}).on("select2:select select2:unselect", function (evt) {
    var _id = evt.params.data._resultId.match(/select2-(\w+)-result/)[1];
    var ul = $(`ul#select2-${_id}-container`);
    ;(ul.data('tmpord')||[]).forEach(tp => {
        var li = ul.children(`li[title="${tp}"]:contains("${tp}")`)
        li.detach().appendTo(ul)
    })
});

devmonkey22 added a commit to devmonkey22/select2 that referenced this issue Mar 31, 2021
…roposal to resolve select2#3106 with some limitations for manual selection changes (as noted).

- This class has to keep both display and selection ordering for all options in order to manage selection order, while maintaining the default results/query ordering (when `sorter` is not used, etc).
- Notable but unfortunate limitation: Setting the control value using `jQuery.val([...])` with multiple items or manually toggling option `selected` (without a `change` event) still cannot preserve the value ordering.
  - The partial workaround for this is to manually set your selected options with the `data-selection-order` attribute before setting val() if possible.
  - It may also be possible to enhance the decorator to watch for manual changes via MutationObserver. TBD.

New options:
- `keepSelectionOrder`
- `trackManualSelectionOrder`

Update docs.
Add tests.
@guisantos
Copy link

I found a quite good solution, with version 4.0.13 (i'm not sure about the older one) we can attach a sorter function that is executed when you open the dropdown, so I basically created my own custom sorter and every time I open the dropdown I sort it alphabetically.

For the select event, this code is to append the last selected item to the end of the input list.

.on('select2:select', function (e) {
       //Append selected element to the end of the list, otherwise it follows the same order as the dropdown
       var element = e.params.data.element;
       var $element = $(element);
       $(this).append($element);
       $(this).trigger("change");
})

and for the sorter, you can pass a function as an option and it's called in the Results.prototype.sort of Select2.

$('#id').select2({
       sorter: sorter 
})

Sorter function receive the data (array of objects that is used to create the options), I use my custom sort method to order alphabetically before return it.

function sorter(data) {
       return data.sort(customSort);
}
function customSort(a, b) {
       if (a.text < b.text) {
            return -1;
        }
        if (a.text > b.text) {
            return 1;
        }
        return 0;
}

I think it's an easy solution, and it is working well in my scenario.

@devmonkey22
Copy link
Contributor

@guisantos Your approach generally works fine unless you also want the field's value via things like $('#id').val() to keep the user's selection order. Without something more invasive to manage <options>, browsers/jQuery iterate the list of options in DOM order to return the list of selected items. If you don't need this, then you're all set.

contactjavas added a commit to mapsteps/ultimate-dashboard that referenced this issue Aug 3, 2021
If you're interested, you can see multiselect selection order issue here:
select2/select2#3106

We don't use the solution there, since I couldn't find perfect solution there. So we built our solution to match our need.
@erlangparasu
Copy link

var select = $('#input_select_abc');
select.on('select2:unselecting', function (event) {
    console.log('unselecting:', event.params.args.data.id);
    select.find('option[value="' + event.params.args.data.id + '"]').remove();
});

@bayukartiko
Copy link

bayukartiko commented Feb 3, 2023

maybe I'm too far-away late and this topic looks out-of-date, but here is my code:

    $('select').select2({
        placeholder: 'placeholder',
        allowClear: true,
        closeOnSelect: false,
    }).on('change', (e) => {
        // ... do your stuff
    });

    $('select').on('select2:select', (evt) => {
        var element = evt.params.data.element;
        var $element = $(element);
                                
        window.setTimeout(function () {
            if ($('select').find(':selected').length > 1) {
                var $selected = $('select').find(':selected');
                var $last = $selected.eq(-$selected.length);

                $element.detach();
                $last.before($element);
             } else {
                $element.detach();
                $('select').prepend($element);
             }
                        
             $('select').trigger('change');
         }, 1);
     });

     $('select').on('select2:unselect', (evt) => {
         if ($('select').find(':selected').length) {
             var element = evt.params.data.element;
             $('select').prepend(element);
         }
     });

what I do in here is i found answer from @kevin-brown and copy-paste from http://jsbin.com/cizehajema/2/edit and improve some logic.

in the end, when the user clicked option, find where the oldest to latest choice then order it by latest choices. And it will appear in the first choices. Take a look at my example screenshot here:
image
I select "Laptop" -> "PC" -> "Software" and my code work very well with what this topic trying to solve.


ADDITION:
I also making custom logic such as when the selected option choices is more than specific limit (in my case is > 2), then hide the rest of oldest choices. let's take a look an more addition of my code above:

    $('select').on('select2:opening', () => {
        $('.select2-selection__choice').show();
        $('.select2-selection__choice_more').remove();
     });
     $('select').on('select2:closing', () => {
         $('.select2-selection').width('auto');

         var $choice = $('.select2-selection__choice');

         $choice.first().show();

         $choice.slice(1,2).show();
         $choice.slice(2).hide();

         $choice.eq(2).after(`<li class='select2-selection__choice select2-selection__choice_more'>...</li>`);
     });

the final result is approxiately like this below:
image

thank you
version information:
select2: ^4.0.13

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.