Select2 with createSearchChoice uses newly created choice for keyboard entry even given a match, bug or am I missing something?
Asked Answered
V

3

16

I'm using Select2 (version 3.4.0) to populate a tag list. The tags are matched against existing ones via ajax call, and I'm using createSearchChoice to allow creating new tags. The code works so far, and looks something like this:

$(mytags).select2({
    multiple: true,
    placeholder: "Please enter tags",
    tokenSeparators: [ "," ],
    ajax: {
        multiple: true,
        url: myurl,
        dataType: "json",
        data: function(term, page) {
            return {
                q: term
            };
        },
        results: function(data, page) {
            return data;
        }
    },
    createSearchChoice: function(term) {
        return {
            id: term,
            text: term + ' (new)'
        };
    },
});

All pretty standard, except note the appended (new) in createSearchChoice. I need users to know that this is not a preexisting tag.

It works as expected: if I start typing "new-tag", I get "new-tag (new)" tag suggested at the top of the list, and if I pick it, the tag list contains "new-tag (new)", as expected. If the tag already exists, Select2 detects the match, and no "(new)" choice is created. Pressing return or clicking on the match works as expected.

The problem appears when I type a comma (my single tokenSeparators entry) while there is a match. Select2 closes that token, and adds the tag to the list, but with the "(new)" label appended, i.e. it uses the return value from createSeachChoice even if it does not have to.

Is this a bug in Select2, or am I using it wrong (and what should I do instead)?

Vivacious answered 17/5, 2013 at 9:48 Comment(0)
R
17

I 'm not sure if this is a bug or not -- in any case, there is no open issue referring to this behavior at the GitHub issue tracker at this moment.

You can mostly fix the behavior yourself though. The idea is that the createSearchChoice callback must be able to tell if term refers to a search result or not. But createSearchChoice does not have direct access to the search results, so how can we enable that? Well, by saving the latest batch of search results from inside the results callback.

var lastResults = [];

$(...).select2({
    ajax: {
        multiple: true,
        url: "/echo/json/",
        dataType: "json",
        type: "POST",
        data: function (term, page) {
            return {
                json: JSON.stringify({results: [{id: "foo", text:"foo"},{id:"bar", text:"bar"}]}),
                q: term
            };
        },
        results: function (data, page) {
            lastResults = data.results;
            return data;
        }
    },
    createSearchChoice: function (term) {
        if(lastResults.some(function(r) { return r.text == term })) {
            return { id: term, text: term };
        }
        else {
            return { id: term, text: term + " (new)" };
        }
    }
});

This code uses Array.some so you need something better than IE8 (which is the select2 minimum requirement) to run it, but of course it is possible to emulate the behavior.

See it in action.

There is, however, a fly in the ointment: this code works correctly only if the search results corresponding to the current search term have been already received.

This should be obvious: if you type very fast and create a search term that corresponds to an existing tag but hit comma before the search results that include that tag have arrived, createSearchChoice will be testing for the tag's presence among the previously received search results. If those results do not include the tag, then the tag will be displayed as "new" even though it is not.

Unfortunately I don't believe there is anything you can do to prevent this from happening.

Refit answered 31/5, 2013 at 13:0 Comment(6)
Thanks, in my case, the results should be there fast enough, and as long as it works in most cases, that should be enough.Vivacious
Great answer Jon. I have noticed that in your fiddle if you enter "bar" and hit enter on your keyboard then "foo" will be selected which I would think is not the desired behaviour.Cochin
Since this answer was written, an issue was added to GitHub for this: github.com/ivaynberg/select2/issues/794. I've got a basic implementation of the suggestion there working and will probably submit a pull request soon (I'll comment at the above link when I do).Lynnet
This is a great answer! I noticed ( and it may be just i'm using a newer version ) that you don't need to store in that lastResults. If you do createSearchChoice: function (term, data) then data will contain what you need just as lastResults did. Thanks for the helpSacellum
@jacklin: I dug into the source a bit and came out with mixed feelings: on the one hand, it does work exactly like you say. OTOH, createSearchChoice is also called by the default tokenizer with the current selection as the second argument. So unfortunately I don't think we can unconditionally advertise that as a solution, much as it would be nice to (in addition, this argument is not documented -- is that on purpose or a doc bug?).Refit
can you please suggest me any documentation related this createSearchChoice or any related link to the quick lookout. Thank You.Travis
H
5

Instead of tweeking the result, I think it is better to work on the server side.

If the server doesn't find a tag make it return a json answer with the new tag

{"more":false,"results":[{"id":"whatever","text":"new-tag (new)"}]}
Hauser answered 24/2, 2014 at 16:8 Comment(0)
D
2

There is another parameter for the 'createSearchChoice' - 'page', it lists all the choices, you can easily find dupes with it.

createSearchChoice = function (term, page) {
    if( page.some(function(item) {
        return item.text.toLowerCase() === term.toLowerCase();
    }) ){
        return { val: term, name: term + '*' };
    }
}
Descend answered 16/10, 2014 at 14:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.