Reputation: 32614
Background
Currently in a project of mine I am using jQuery autocomplete
for a few fields.
To provide context, the application records Runs
. Each Run
must have a Route
associated with it. A Route
meaning, where the user ran.
When the user types in a Route
, a list of their Routes
gets displayed by the autocomplete
options, but the database requires the RouteID
for validation.
To compensate for this, I stored the RouteID
in a HiddenFor
HtmlHelper. When the user selects the route from the autocomplete
, the HiddenFor
gets assigned.
What my problem is
If the user types in the full name of the Route
, instead of selecting it from the autocomplete
list or enters a Route
that does not exist, the HiddenFor
will not get assigned. When this happens, I have to find the Route
by its name and validate that it exists on the server.
I would like to not have to create this work-around for every autocomplete
.
The bottom line
Is there anyway to make the autocomplete
act more like a select list
? I want the user to have no choice but to select the text of one option from the autocomplete
list, and have the value of the selected option is sent to the server.
If I have to stick to the HiddenFor
method, is there at least a way to force the user to select an option from the autocomplete
list?
Below is the code I am currently using
Mark-up
@Html.LabelFor(model => model.RouteID, "Route")
<input type="text" data-autocomplete-url="@Url.Action("../Route/GetRoutesByUser")" />
@Html.HiddenFor(m => m.RouteID)
jQuery
$('*[data-autocomplete-url]')
.each(function () {
$(this).autocomplete({
source: $(this).data("autocomplete-url"),
minLength: 2,
select: function (event, ui) {
log(ui.item.id, ui.item.name);
}
});
});
Code
public ActionResult GetRoutesByUser(string term)
{
var routeList = db.Routes.Where(r => r.Name.Contains(term))
.Take(5)
.Select(r => new { id = r.RouteID, label = r.Name, name = "RouteID"});
return Json(routeList, JsonRequestBehavior.AllowGet);
}
Upvotes: 2
Views: 4270
Reputation: 32614
Alright, after a lot of fiddling around I came up with the following implementation:
The code below is a HtmlHelper
called @Html.AutocompleteWithHiddenFor
. The HtmlHelper will create an input
HTML element with a data-autocomplete-url
property based on the controller
and action
passed in.
If the input
element needs a value
then you can pass that in as well. A HiddenFor
will be created for the Model
property passed in and a ValidationMessageFor
will be created for the Model
as well.
Now all I have to do is use @Html.AutocompleteWithHiddenFor
, and pass in whatever expression I need with the controller and action (and possibly the value) to get the autocomplete functionality and have the ID pass to the server instead of the text.
jQuery
$(function () {
function log(id, name) {
var hidden = $('#' + name);
hidden.attr("value", id);
}
$('*[data-autocomplete-url]')
.each(function () {
$(this).autocomplete({
source: $(this).data("autocomplete-url"),
minLength: 2,
select: function (event, ui) {
log(ui.item.id, ui.item.name);
},
change: function (event, ui) {
if (!ui.item) {
this.value = '';
} else {
log(ui.item.id, ui.item.name);
}
}
});
});
});
AutocompleteHelper class
public static class AutocompleteHelper
{
/// <summary>
/// Given a Model's property, a controller, and a method that belongs to that controller,
/// this function will create an input html element with a data-autocomplete-url property
/// with the method the autocomplete will need to call the method. A HiddenFor will be
/// created for the Model's property passed in, so the HiddenFor will be validated
/// and the html input will not.
/// </summary>
/// <returns></returns>
public static MvcHtmlString AutocompleteWithHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, string controllerName, string actionName, object value = null)
{
// Create the URL of the Autocomplete function
string autocompleteUrl = UrlHelper.GenerateUrl(null, actionName,
controllerName,
null,
html.RouteCollection,
html.ViewContext.RequestContext,
includeImplicitMvcValues: true);
// Create the input[type='text'] html element, that does
// not need to be aware of the model
String textbox = "<input type='text' data-autocomplete-url='" + autocompleteUrl + "'";
// However, it might need to be have a value already populated
if (value != null)
{
textbox += "value='" + value.ToString() + "'";
}
// close out the tag
textbox += " />";
// A validation message that will fire depending on any
// attributes placed on the property
MvcHtmlString valid = html.ValidationMessageFor(expression);
// The HiddenFor that will bind to the ID needed rather than
// the text received from the Autocomplete
MvcHtmlString hidden = html.HiddenFor(expression);
string both = textbox + " " + hidden + " " + valid;
return MvcHtmlString.Create(both);
}
}
View
@Html.LabelFor(model => model.RouteID, "Route")
@Html.AutocompleteWithHiddenFor(model => model.RouteID, "Route", "GetRoutesByUser")
Or if it needs a value
@Html.LabelFor(model => model.Route, "Route")
@Html.AutocompleteWithHiddenFor(model => model.RouteID, "Route", "GetRoutesByUser", @Model.RouteName)
Upvotes: 1
Reputation: 126072
I would use the change
event for this, clearing the value of the input
if an item was not selected:
$('*[data-autocomplete-url]')
.each(function () {
$(this).autocomplete({
source: $(this).data("autocomplete-url"),
minLength: 2,
select: function (event, ui) {
log(ui.item.id, ui.item.name);
},
change: function (event, ui) {
if (!ui.item) {
this.value = '';
} else {
// Populate your hidden input.
}
}
});
});
});
Example: http://jsfiddle.net/urEzm/
Upvotes: 1