I am working on an app that uses Select2 (version 3.5.1). The HTML to setup this drop down / autocomplete field looks like this:
<input id="mySelect" class="form-control" type="hidden">
The form-control
class in this snippet comes from Bootstrap. I am initializing this field from JavaScript using the following:
function getItemFormat(item) { var format = '<div>' + item.ItemName + '</div>'; return format; } $(function() { $('#mySelect').select2({ minimumInputLength: 5, placeholder: 'Search for an item', allowClear: true, ajax: { url: '/api/getItems', dataType: 'json', quietMillis: 250, data: function (term, page) { return { query: term }; }, results: function (data, page) { return { results: data, id: 'ItemId', text: 'ItemText' }; } }, formatResult: getItemFormat, dropdownCssClass: "bigdrop", escapeMarkup: function (m) { return m; } }); });
When my select field loads, it successfully renders. Once I type at least the fifth character, it successfully pulls items from the server and lists them as options. However, if I try to select one of them, nothing happens. The drop-down popup stays open. Nothing gets put in the actual field. There are no errors in the JavaScript console. Its like I didn't click anything.
In addition, I noticed that nothing is highlighted when I put my mouse over an item or attempt to navigate the list of options with the arrow keys.
What am I doing wrong?
Select2 does not function properly when I use it inside a Bootstrap modal. This issue occurs because Bootstrap modals tend to steal focus from other elements outside of the modal. Since by default, Select2 attaches the dropdown menu to the <body> element, it is considered "outside of the modal".
By default, Select2 will attach the dropdown to the end of the body and will absolutely position it to appear above or below the selection container. Select2 will display the dropdown above the container if there is not enough space below the container, but there is enough space above it.
New options can be added to a Select2 control programmatically by creating a new Javascript Option object and appending it to the control: var data = { id: 1, text: 'Barn owl' }; var newOption = new Option(data. text, data.id, false, false); $('#mySelect2'). append(newOption).
By default, results
of the object you are returning in ajax.results
should be an array in this structure [{id:1,text:"a"},{id:2,text:"b"}, ...]
.
results: function (data, page) { var array = data.results; //depends on your JSON return { results: array }; }
In Select2.js it actually states:
* @param options.results a function(remoteData, pageNumber, query) that converts data returned form the remote request to the format expected by Select2. * The expected format is an object containing the following keys: * results array of objects that will be used as choices * more (optional) boolean indicating whether there are more results available * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
Reading the source code, we can see that ajax.results
is called on AJAX success:
success: function (data) { // TODO - replace query.page with query so users have access to term, page, etc. // added query as third paramter to keep backwards compatibility var results = options.results(data, query.page, query); query.callback(results); }
So ajax.results
is really just a function for you to format your data into the appropriate structure ( e.g. [{id:a,text:"a"},{id:b,text:"b"}, ...]
) before the data is passed to query.callback
:
callback: this.bind(function (data) { // ignore a response if the select2 has been closed before it was received if (!self.opened()) return; self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context}); self.postprocessResults(data, false, false); if (data.more===true) { more.detach().appendTo(results).html(self.opts.escapeMarkup(evaluate(self.opts.formatLoadMore, self.opts.element, page+1))); window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); } else { more.remove(); } self.positionDropdown(); self.resultsPage = page; self.context = data.context; this.opts.element.trigger({ type: "select2-loaded", items: data }); })});
And what query.callback
eventually does is to set the logic up properly so that everything works fine when you choose one of the items and trigger .selectChoice
.
selectChoice: function (choice) { var selected = this.container.find(".select2-search-choice-focus"); if (selected.length && choice && choice[0] == selected[0]) { } else { if (selected.length) { this.opts.element.trigger("choice-deselected", selected); } selected.removeClass("select2-search-choice-focus"); if (choice && choice.length) { this.close(); choice.addClass("select2-search-choice-focus"); this.opts.element.trigger("choice-selected", choice); } } }
So if there is some misconfiguration (e.g. results
is not in the correct structure) that causes the class .select2-search-choice-focus
not to be added to the DOM element before .selectChoice
is called, this is what happens:
The drop-down popup stays open. Nothing gets put in the actual field. There are no errors in the JavaScript console. Its like I didn't click anything.
There are many solutions to this. One of them is, of course, do some array keys manipulation in ajax.results
.
results: function (data, page) { //data = { results:[{ItemId:1,ItemText:"a"},{ItemId:2,ItemText:"b"}] }; var array = data.results; var i = 0; while(i < array.length){ array[i]["id"] = array[i]['ItemId']; array[i]["text"] = array[i]['ItemText']; delete array[i]["ItemId"]; delete array[i]["ItemText"]; i++; } return { results: array }; }
But you may ask: why must the id be "id" and the text be "text" in the array?
[{id:1,text:"a"},{id:2,text:"b"}]
Can the array be in this structure instead?
[{ItemId:1,ItemText:"a"},{ItemId:2,ItemText:"b"}]
The answer is yes. You just need to overwrite the id
and text
functions with your own functions.
Here are the original functions for .selecte2
in Select2.js:
id: function (e) { return e == undefined ? null : e.id; }, text: function (e) { if (e && this.data && this.data.text) { if ($.isFunction(this.data.text)) { return this.data.text(e); } else { return e[this.data.text]; } } else { return e.text; } },
To overwrite them, just add your own functions inside the object you are passing to .selecte2
:
$('#mySelect').select2({ id: function (item) { return item.ItemId }, text: function (item) { return item.ItemText } ...... });
However, the text of the selected item does not appear in the field after the list closes.
This means .selectChoice
has been successfully executed. Now the problem lies in .updateSelection
. In the source code:
updateSelection: function (data) { var container=this.selection.find(".select2-chosen"), formatted, cssClass; this.selection.data("select2-data", data); container.empty(); if (data !== null) { formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup); } if (formatted !== undefined) { container.append(formatted); } cssClass=this.opts.formatSelectionCssClass(data, container); if (cssClass !== undefined) { container.addClass(cssClass); } this.selection.removeClass("select2-default"); if (this.opts.allowClear && this.getPlaceholder() !== undefined) { this.container.addClass("select2-allowclear"); } }
From here we can see that, before the corresponding string of text is placed into the input, it would call formatSelection
.
formatSelection: function (data, container, escapeMarkup) { return data ? escapeMarkup(this.text(data)) : undefined; },
Previously I thought this.text(data)
can be overwritten by having text: funcion(item){ ... }
in the parameters, but sadly it doesn't work that way.
Therefore to render the text properly in the field, you should overwrite formatSelection
by doing
$('#mySelect').select2({ id: function (item) { return item.ItemId }, formatSelection: function (item) { return item.ItemText } //...... });
instead of trying to overwrite text
(which should supposedly have the same effect but this way of overwriting is not yet supported/implemented in the library)
$('#mySelect').select2({ id: function (item) { return item.ItemId }, text: function (item) { return item.ItemText } //this will not work. //...... });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With