(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.