Donnerstag, 3. März 2011

Lift and AutoComplete

The Lift-Web-Framework allows you easy access to JQuery's AutoComplete widget. If you have used Google, you might agree that the drop down list which is populated with entries depending on your current input is a nice thing to have.

Here is how you integrate it into your Lift application.

First you have to add a dependency to your project (in this case Lift 2.2, using Scala 2.8.0):

If you use SBT in ./project/build/Project.scala

override def libraryDependencies = Set(
...
"net.liftweb" % "lift-widgets_2.8.0" % “2.2” % "compile->default",

In a Maven project this should translate to (in pom.xls - not tested, I'm using SBT):



<dependency>
<groupid>net.liftweb</groupid>
<artifactid>lift-widgets</artifactid>
<version>2.2-scala280</version>
</dependency>


The first step in your application is the initialisation, by adding the following code – usually into into Boot.scala

import _root_.net.liftweb.widgets.autocomplete._

class Boot {
def boot {

AutoComplete.init

In your template, the widget is represented like any other input field with a tag of your choice. This example uses

Here the snippet code to bind it:

Helpers.bind("dt", in,
...
"autocomplete" -> AutoComplete(startValue,
buildQuery _,
value => processResult(value)
),
...

where:
startValue is a string for the initial value of the AutoComplete text field.
buildQuery is a function to populate the drop down list.
processResult is a function called when the form is submitted, containing the value of the widget (or not, see bug report below).

Here is a simple example of processResult

def processResult(s : String) = {
println("%s".format(s))
}

The interesting part is the buildQuery function. It has the following signature:

def buildQuery(current: String, limit: Int): Seq[String] = {
...
}

where
current is the string typed into the autoComplete text field.
limit is an integer to limit the number of Strings returned by the function

A valid but not very useful implementation would be (from http://demo.liftweb.net/ajax)

def buildQuery(current: String, limit: Int): Seq[String] = {
(1 to limit).map(n => current+""+n)
}

Most application however will query a database. Lets assume that Mydatabase has a field called name (e.g. with the names of employees). A query might look like this:

def buildQuery(current: String, limit: Int): Seq[String] = {
Mydatabase.findAll(
Like(Mydatabase.name,(current + "%")),
OrderBy(Mydatabase.name,Ascending),
MaxRows(limit)
).map( _.name.is)
}

This will findAll names in Mydatabase which start with the currently entered string, sorts them, and limit the number of results.

That's all you need.

Bug or a feature?

The current implementation of the AutoComplete functionality (Lift 2.2) only allows the selection from a drop down list as a valid input. This sounds reasonable, but it also prevents you from clearing the input after you have made a selection. This can lead to the following situation:

At a shopping site, the user selects an article via an autocomplete field. Then he changes his mind and clears the input field using the delete key. When he clicks on the submit button the previous selected article is sent to the server, even if the screen shows an empty text field.

With the help of Sergey Andreev, I have developed a small patch which has also been published in Lift ticket 892. Even though it is only 8 lines long, it is unlikely to make it into Lift due to Intellectual Property policy consideration by DPP.

The patched version is available on GitHub.

If you copy it into your project, you might want to change the package name (first and last three lines) and replace

import _root_.net.liftweb.widgets.autocomplete._

with your package name.

Keep in mind that the returned result may contain strings (including an empty string) that might be invalid.