The wheel isn't round enough (or, "RTFW, Pete")
(This has been in my drafts for too long…)
I have a need for an auto-complete input on one of the pages that I’m building at work. I have a love for jQuery, so a quick google around yielded jQuery AutoComplete and CodeAssembly’s, but neither (on cursory inspection; more later…) did all of what I wanted (and, to be brutally honest, it’s been a while since I wrote something new in JavaScript, so I had an ulterior motive for concluding that…).
My requirements look something like:
- Anchor onto an input element (so, in the absence of JS, the user can just type into it)
- Caches on the client-side - not for performance, but rather for UI responsiveness
- Should filter and select on the client-side against cache while the user is typing (that is, if possible, narrow down the list)
- CSS-able - should be possible for it to end up looking and behaving like a select element (or, with a bit of extension, a combobox)
- Keyboard support
- Shouldn’t hit the site until the user has finished typing
- Should assume the site’s going to give back a list of JSON-encoded objects to play with
- Should have different match-modes (contains, starts-with…)
A little while later, I took CodeAssembly’s plugin as a starting point and began to roll my own version. I should mention that appearances to the contrary, I’m actually far more in favour of “buy-or-find” than “build” most of the time. So, looking at the starting point (and, remember, this is me reminding myself of how this JS thing works after a few months of nothing but C#), I decided that TypeWatch did a better job of the “wait until the user is done typing before calling back” than what was already present.
Another google flies past, and I remember jQuery has a HotKeys plugin. This is also duly incorporated (and pretty painlessly).
Now, I want it to cache data on the client-side to not bother the server-side more than it needs to.
Hmm. This is getting a bit long, now. Am I sure the existing wheels aren’t round enough? Hit google. What’s this? jQuery autocomplete + JSON + ASP.NET MVC? That sounds rather handy. Oh; look, it’s using the jQuery autocomplete plugin that I initially discarded. Oh! There’re some undocumented features! So instead of writing it myself, I end up with the following in my view:
$(function() {
$(".country input[name=dto.Country]")
.bind("result", function(e, data, text) {
$(".country input[type=hidden]").val(data.id);
})
.autocomplete(
"${Url.RouteUrl(new {Controller='Common', Action='Countries'})}"
, {
dataType: "json"
,parse: function(data) {
var rows = [];
$.each(data, function(i) {
rows[i] = { data: this, value: this.name, result: this.name }
});
return rows;
}
,formatItem: function(row, i, n) {
return row.name;
}
});
});
<li class="country">
<label for="dto.Country">Country:</label>
!{Html.TextBox("dto.Country")}
!{Html.Hidden("dto.CountryId")}
!{Html.ValidationMessage("dto.Country")}
</li>
(I’m on ASP.NET MVC beta, and I do hope they’re going to fix the Html helper such that it generates id attributes that are actually XHTML compliant - “#dto.Country” as a CSS selector actually means “target the element with id ‘dto’ and class ‘Country’…)
The result of this is that the autocomplete is wired to the visible text-box, and when the user selects a value, the hidden input receives the country’s ID. The data-transfer object that the controller-action is using has both Country (in case of a revisit via failed server-side validation) and CountryId (the value we’re actually interested in, business-logic wise) properties. The bind to the “result” event was also undocumented; I had to pore through the source of autocomplete (which was a good move; it’s written really cleanly, and that reinforces the confidence I have in it).
The ~/common/countries controller action looks like:
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult Countries(string q, int limit)
{
var countries = new JsonResult
{
Data = Tvsiab.GetTable<country>()
.Where(c => c.Name.StartsWith(q))
.Take(limit)
//.Distinct(countryComparer) // it seems LinqToSql's query-provider doesn't support this part of the Linq API!
.OrderBy(o => o.Name)
.Select(c => new { id = c.PKid, name = c.Name })
};
return countries;
}
which spits back an array of id,name
JSON objects.