Introduction
jQuery has a auto-complete pluggin in its jquery-UI collection. We can use that for having a multi-select list or single select autocomplete using our own datasource. This post will showcase methodology to create a wrapper library using auto-complete and necessary steps to take to make your own auto-complete rock and roll.
- Add a static script function in codebehind which returns desired data. Annotate it with ScriptMethod attribute. The function should return an
IEnumerable<T>
;
where T: List<string>
for plain text display; Dictionary<KeyValuePair>
for a display and a value field; or Tuple<T1,.. Tn> <entity>where one field
is for display and other as value fields.
public partial class _Default : Page
{
[System.Web.Script.Services.ScriptMethod (UseHttpGet = true,
ResponseFormat = System.Web.Script.Services.ResponseFormat.Json)]
public static IEnumerable> GetAllDepartments()
{
return Repository.GetAllDepartments();
}
}
public static class Repository
{
internal static IEnumerable> GetAllDepartments()
{
}
}
- Add files jquery.ui.css and jquery.ui.js in the header section along with
jquery.js.
- Declare an array within JavaScript for each type of entity to be bound.
var dept = [];
var people = [];
- Call the script function through ajax, and on success, typecast the JSON to required format. The AutoComplete library conventionally the display field
to have “label” field and other properties to be “value”, “value1”.... Hence the casting is required to such format, and each casted object is stored
in a predefined array; which serves as the datasource for the autocomplete dropdown.
Alternatively, if a long list of items need to be binded (through a Tuple<T1... Tn), create own JSON object without any need to cast to label/value (although that would
more suit the convention. The AJAX method shown takes the datasource object to be populated and the corresponding method in the Script Service involved.
This enables a single function to be generalized for populating as many datasources as required.
function PopulateItems(arrayRef, scriptName) {
var serviceUrl = '<=ResolveClientUrl("~//MyPage.aspx")%>' + "/" + scriptName;
$.ajax({
url: serviceUrl,
type: "POST",
contentType: "application/json; charset=utf-8",
processData: true,
success: function (output) {
if (output.d.length > 0) {
arrayRef.length = 0;
$(output.d).each(function () {
arrayRef.push({
"value": this.Value,
"label": this.Key
});
});
}
},
error: function OnError(request, status, error) {
alert(error.statusText);
}
});
}
- Add autocomplete filter override to match start of word rather than any place.
$.ui.autocomplete.filter = function (array, term) {
var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
$.grep(array, function (value) { return matcher.test(value.label); });
};
- Add input fields/textbox wrapped in a
<div class="ui-widget">
to load up the auto-complete widget. Only the display purpose control is visible,
the value purpose fields are hidden.
<div class="ui-widget">
<input type="text" runat="server" id="txtDept" />
<input type="text" runat="server" id="txtDeptValue" style="display: none" />
</div>
<div class="ui-widget">
<input type="text" runat="server" id="txtPersonName" />
<input type="text" runat="server" id="txtPersonId" style="display: none" />
</div>
- Bind text control input events:
- Keydown:
- Tab: show autocomplete
- Delete / backspace key: delete only the last item selected.
$("#" + textControlId)
.bind("keydown", function (event) {
if ((event.KeyCode == $.ui.keyCode.TAB) &
$(this).data("ui-autocomplete").menu.active)) {
event.preventDefault();
}
RemoveItem(isMulti, event, textControlId, valueControlId);
});
- AutoComplete
- Source: datasource direct or filtered (for multi-item)
- Focus: disable event to prevent value insertion
- Select: map functionality to select the text (single/multi) and the corresponding value fields (if any).
.autocomplete({
minLength: 2,
source: datasource,
focus: function (event, ui) { return false; },
select: function (event, ui) {
$("#" + textControlId).val(ui.item.label);
$("#" + valueControlId).val(ui.item.value);
return false;
}
});
- Data: render autocomplete list as a dropdown
.data("ui-autocomplete")._renderItem = RenderItem;
- Now bind the functions in the ready handler.
$(document).ready(function() {
PopulateItems(dept, "GetAllDeptNames");
PopulateItems(people, "GetAllPeopleNames");
ToBeExecutedOnDOMReady(true, dept,
"<%=txtDept.ClientID%>",
"<%=txtDeptValue.ClientID%>");
ToBeExecutedOnDOMReady(false, people,
"<%=txtPersonName.ClientID%>",
"<%=txtPersonId.ClientID%>");
};
);
The code has been written to allow a single item list as well as a multi item list. To enable singe/multi, pass the first parameter true/false accordingly.
The full code along with all the helper functions is listed below:
var dept = [];
var people = [];
function split(csv) {
return csv.split(/;\s*/);
};
function SetSelectedValue(sourceType, textControlId, valueControlId, csv) {
var datasource;
switch (sourceType) {
case 1: datasource = dept; break;
case 2: datasource = people; break;
}
var selectedValues = split(csv); var itemId = "";
var itemName = "";
var i = datasource.length;
for (var iter in selectedValues) {
var value = selectedValues[iter];
while (i--) {
var currItem = datasource[i];
if (currItem.value == value) {
itemId = currItem.value + ";" + itemId;
itemName = currItem.label + ";" + itemId;
break;
}
}
}
itemId = itemId.replace(/^\s+l;\s+$/g, "");
itemName = itemName.replace(/^\s+l;\s+$/g, "");
$("#" + textControlId).val(itemName);
$("#" + valueControlId).val(itemId);
}
function PopulateItems(arrayRef, scriptName) {
var serviceUrl = '<%=ResolveClientUrl("~//Service/Services.aspx")%>' + "/" + scriptName;
$.ajax({
url: serviceUrl,
type: "POST",
contentType: "application/json; charset=utf-8",
processData: true,
success: function (output) {
if (output.d.length > 0) {
arrayRef.length = 0;
$(output.d).each(function () {
arrayRef.push({
"value": this.Value,
"label": this.Key
});
});
}
},
error: function OnError(request, status, error) {
alert(error.statusText);
}
});
}
function ExtractLastTerm(term) {
return split(term).pop();
}
function ClearValues(textControlId, valueControlId) {
$("#" + textControlId).val("");
$("#" + valueControlId).val("");
}
function RemoveItem(isMulti, event, textControlId, valueControlId) {
if (isMulti) {
if (event.KeyCode == 8) {
var currText = $("#" + textControlId).val();
var items = split(currText);
items.pop();
items.push("");
$("#" + textControlId).val(items.join(";"));
var currIds = $("#" + valueControlId).val();
items = split(currIds);
items.pop();
items.push("");
$("#" + valueControlId).val(items.join(";"));
event.preventDefault();
return false;
} else if (event.KeyCode == 46) {
var check = confirm("Only last item can be deleted (using backspace).
Click [OK] to delete entire selection or [Cancel] to review.");
if (check)
ClearValues(textControlId, valueControlId);
event.preventDefault();
return false;
}
} else {
if ((event.KeyCode == 46) || (event.KeyCode == 8)) {
ClearValues(textControlId, valueControlId);
event.preventDefault();
return false;
}
}
}
function RenderItem(ul, item) {
return $("<li>").append("<a>" + item.label + "</a>").appendTo(ul);
}
$.ui.autocomplete.filter = function (array, term) {
var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
$.grep(array, function (value) { return matcher.test(value.label); });
};
function ContainedInArray(arrayRef, item) {
for(index in arrayRef) {
if (arrayRef[index].value == item) {
return true;
}
}
return false;
}
function ToBeExecutedOnDOMReady(isMulti, datasource, textControlId, valueControlId) {
$(document).ready(function () {
if (("#" + textControlId).length > 0) {
if (isMulti) {
$("#" + textControlId)
.bind("keydown", function (event) {
if ((event.KeyCode == $.ui.keyCode.TAB) &&
($(this).data("ui-autocomplete").menu.active)) {
event.preventDefault();
}
RemoveItem(isMulti, event, textControlId, valueControlId);
})
.autocomplete({
minLength: 2,
source: function (request, response) {
response($.ui.autocomplete.filter(datasource, Ext
},
focus: function (event, ui) {
return false;
},
select: function (event, ui) {
var isPresent = 1;
var text = $("#" + textControlId).val();
var arrText = split(text);
if (arrText.length > 0) {
isPresent = ContainedInArray(arrText, ui.item.label);
}
var items = split(this.Value);
items.pop();
if (!isPresent) {
items.push(ui.item.label);
items.push("");
}
$("#" + textControlId).val(items.join(";"));
if (!isPresent) {
var currItem = $("#" + valueControlId).val();
items.push(ui.item.label);
items.push("");
}
$("#" + textControlId).val(items.join(";"));
}
})
.data("ui-autocomplete")._renderItem = RenderItem;
}
else {
$("#" + textControlId)
.bind("keydown", function (event) {
if ((event.KeyCode == $.ui.keyCode.TAB) &&
($(this).data("ui-autocomplete").menu.active)) {
event.preventDefault();
}
RemoveItem(isMulti, event, textControlId, valueControlId);
})
.autocomplete({
minLength: 2,
source: datasource,
focus: function (event, ui) { return false; },
select: function (event, ui) {
$("#" + textControlId).val(ui.item.label);
$("#" + valueControlId).val(ui.item.value);
return false;
}
})
.data("ui-autocomplete")._renderItem = RenderItem;
}
}
});
};
$(document).ready(function() {
PopulateItems(dept, "GetAllDeptNames");
PopulateItems(people, "GetAllPeopleNames");
ToBeExecutedOnDOMReady(true, dept, "<%=txtDept.ClientID%>", "<%=txtDeptValue.ClientID%>");
ToBeExecutedOnDOMReady(false, people, "<%=txtPersonName.ClientID%>", "<%=txtPersonId.ClientID%>");
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(EndRequestHandler);
function EndRequestHandler(sender, args) {
if (args.get_Error() == undefined) {
PopulateItems(dept, "GetAllDeptNames");
PopulateItems(people, "GetAllPeopleNames");
ToBeExecutedOnDOMReady(true, dept, "<%=txtDept.ClientID%>", "<%=txtDeptValue.ClientID%>");
ToBeExecutedOnDOMReady(false, people, "<%=txtPersonName.ClientID%>", "<%=txtPersonId.ClientID%>");
}
}
});
This example works if the list is small and can be put as a page level variable. However if the list is too huge that getting all items on the page start will be too heavy,
take the datasource as a service method itself. The service will be called through the 'source' parameter and will dynamically update the items.
[Note: This call will be once per character entered, so you need output caching on the service side.]
$("#servicePull")
.autocomplete({
source: function(request, response) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: serviceUrl,
dataType: "json",
data: "{'RequestId':'" + request + "'}",
dataFilter: function(data) { return data; },
success: function(data) {
response($.map(data.d, function(item) {
return {
custId: item.Item1,
custName: item.Item2,
custAdd: item.Item3
};
}));
},
error: function(err) { alert(err); }
})},
select: function(event, ui) {
$("#custId").val(ui.item.custId);
$("#custName").val(ui.item.custName);
$("#custAdd").val(ui.item.custAdd);
}
})
.data("ui-autocomplete")._renderItem = function (ul, item) {
return $("<li>").append("<a>" + item.custName + "</a>").appendTo(ul);
};