15
votes

I am using select2 version 4 with multiple select and I support users adding new tags but I want to prevent people choosing a new tag if that tag already exists on my backend.

Right now if a user enters a tag that already exists and I have tags:true then it shows two items in the dropdown (the existing one and the new one). Here is an example:

enter image description here

as you can see, "testTag2" is a valid tag from my backend so it shows up in the selection but because of the templateResult function and the fact that tags:true it also shows up as a second item (making the user think they can select it as a new tag).

Is there anyway to only show the "NEW" tag choice in the dropdown, if that text is NOT listed in the dropdown as another option?

here is my javascript code:

function SetupAppTags() {
$("#Tags").select2({
    theme: "classic",
    width: "98%",
    tags: true,
    ajax: {
        url: "/Tag/Search",
        dataType: 'json',
        delay: 300,
        data: function(params) {
            return { q: params.term };
        },
        processResults: function(data, params) {
            return { results: data };
        },
        cache: false
    },
    escapeMarkup: function(markup) { return markup; },
    minimumInputLength: 3,
    templateResult: tagFormatResult,
    templateSelection: tagSelectionResult
});
}

 function tagFormatResult(tag) {

if (tag.loading) {
    return "Loading . . . <img src='/Content/Images/ajax-loader.gif' />";
} else {

    if (tag.name) {
        var existsAlready = $("#Tags option[value='" + tag.id + "']").length > 0;
        if (existsAlready) {
            return null;
        }
        return tag.name;
    }

    var length = $('#tagsContainer .select2-selection__choice').filter(function () {
        return $(this).attr("title").toUpperCase() === person.text.toUpperCase();
    }).length;

    if (length == 1) {
        return null;
    }
    return tag.text + " [NEW]";
}

}

2
can it is possible for you to create a fiddle for this? If Ajax is not possible then even simple array of data will be good enough.vijayP

2 Answers

9
votes

According to Select2 Options

Change how options are matched when searching When users filter down the results by entering search terms into the search box, Select2 uses an internal "matcher" to match search terms to results. When a remote data set is used, Select2 expects that the returned results have already been filtered.

Key matcher Value A function taking search params and the data object. Select2 will pass the individual data objects that have been passed back from the data adapter into the matcher individually to determine if they should be displayed. Only the first-level objects will be passed in, so if you are working with nested data, you need to match those individually.

matcher: function (params, data) {
  // If there are no search terms, return all of the data
  if ($.trim(params.term) === '') {
    return data;
  }

  // `params.term` should be the term that is used for searching
  // `data.text` is the text that is displayed for the data object
  if (data.text.indexOf(params.term) > -1) {
    var modifiedData = $.extend({}, data, true);
    modifiedData.text += ' (matched)';

    // You can return modified objects from here
    // This includes matching the `children` how you want in nested data sets
    return modifiedData;
  }

  // Return `null` if the term should not be displayed
  return null;
}

I think this is your typical case ,except that you should replace ("matched") with ("") , and add else to add that "[NEW]" in your case.

The returned data from ajax call should be the input to your matcher.

so your code should look like:

  matcher: function (params, data) {
  // If there are no search terms, return all of the data
  if ($.trim(params.term) === '') {
    return data;
  }

  // `params.term` should be the term that is used for searching
  // `data.text` is the text that is displayed for the data object
  if (data.text.indexOf(params.term) > -1) {
    var modifiedData = $.extend({}, data, true);
   //match was found then just show it.
   // modifiedData.text += ' (matched)';

    // You can return modified objects from here
    // This includes matching the `children` how you want in nested data sets
    return modifiedData;
  }
   else
  {
   //there is not match found , suggest adding NEW Tag.
    modifiedData.text += '[NEW]';
    return modifiedData;
  }

  // Return `null` if the term should not be displayed
  return null;
}
5
votes

Please correct me if I have misunderstood your question. But as per my understanding I have come up with following solution.

For demo purpose I have pre-loaded couple of <option> elements within <select> box and instead of ajax data feed to select2 I am using plain JavaScript array object mimicking ajax response.

The Updated fiddle link: https://jsfiddle.net/vijayP/akyzt9Ld/11/

The HTML is as follows:

<div id="tagsContainer">
    <select id="Tags" multiple="" name="Tags">
       <option value="white">white</option>
       <option value="green">green</option>
    </select>
</div> 

Here we can see that, the last <option> present in dropdown has text green.

JSON data object which mimic ajax response looks like below:

var data = [{ id: 0, name:'yellow', text: 'yellow' }, 
                { id: 1, name:'green', text: 'green' }, 
                { id: 2, name:'cyan', text: 'cyan' }, 
                { id: 3, name:'violet', text: 'violet' }, 
                { id: 4, name:'gray', text: 'gray' }
                ];

Here again the green is at number 2.

select2 initialization and supporting JavaScript code goes like this:

    var uniqueTexts = null; //array for holding unique text
    $("#Tags").select2({
        tags: true,
        data: data,
        templateResult: tagFormatResult,
        escapeMarkup: function (markup) { 
            uniqueTexts = null; 
            return markup; 
        }, 
        matcher: function (params, data) {
            if(!uniqueTexts){
                uniqueTexts = []; 
            }
            var modifiedData = null;

            if ($.trim(params.term) === '' || data.text.indexOf(params.term) > -1) {
                if(uniqueTexts.indexOf(data.text) == -1)
                {
                    modifiedData = data;
                    uniqueTexts.push(modifiedData.text);
                }
            }

            if(modifiedData)
            {
                return modifiedData;
            }

            return null;
        }
    });

    function tagFormatResult(tag) {
        if (tag.loading) {
            return "Loading . . . <img src='/Content/Images/ajax-loader.gif' />";
        } 
        else 
        {
            var length = $('#tagsContainer .select2-selection__choice').filter(function () {
                return $(this).attr("title").toUpperCase() === tag.text.toUpperCase();
            }).length;

            if (length == 1) {
                return tag.text;
            }

            if (tag.text) {

                if(getOptionCount(tag.text) >1)
                    return tag.text;
                else
                    return tag.text + " [NEW]";
            }
            return tag.text + " [NEW]";
        }
    }

    function getOptionCount(tagText)
    {
        var count = 0;
        var selectBoxOptions = $("#Tags option");
        $(selectBoxOptions).each(function(){
            if(tagText == $(this).text())
                count++;//increment the counter if option text is matching with tag text
        });
        return count;
    }

var uniqueTexts is an array to hold unique text to be shown to the end user. While initiating we are setting it to null. So the idea is whenever user focus-in to the select box OR type-in search keyword; matcher: callback gets called for each option. We check each option and see if it is already present in uniqueTexts[] or not. If its a first occurrence then we allow it to show; otherwise we return null to avoid it from showing 2nd time.

I also added a escapeMarkup: callback handler which gets called whenever options are shown to the end user. This is the point when we can again set uniqueTexts to null. This is the complete one round of cycle. Again user can focus-in or type-in to select box.

function tagFormatResult(tag) works as follows:

1) First it checks whether current tag is already been selected or not. If it is already selected then don't add "[NEW]" text to tag.text

2) Secondly it checks whether current tag.text is present as select option text more than once or not. If it is present at more than one places, then also don't add "[NEW]" to tag.text.

3) In all other cases go ahead and add "[NEW]" to tag.text.

I hope this will help you a bit. Updated Link: https://jsfiddle.net/vijayP/akyzt9Ld/11/