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?
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.
//......
});