Sunday 3 July 2011

Simple jQuery contact application

I continue to be impressed with how easy it is to build nice, useful applications with Seaside using the jQuery support.  We have an old Unix character based contact application that those who don't want to change (and you know how you are) continue to use. I gave up on character based interfaces the late 80's, so I created a simple Seaside app to access the contact data.
The contact data is dumped daily into a special character delimited file, with 40 fields per row. Trivial to parse in Smalltalk...
array := aString subStrings: 1 asCharacter.
...where aString is one row of the data file.

Accessing the fields is just an array access, like...

postalCode
^self dataArray at: 16

The collection of contacts (about 8500) is stored in a contact library object, in three collections, each sorted by a key stored on the object as a lower case string (by first name, by last name and by company). At first I tried using some fancy nested tree structure, then I tried large complex key dictionaries, but doing a match on 8500 instances takes less than 10 ms. Trying to optimize that just added complexity. Storing the search keys as lower case strings rather than translating them on each iteration was helpful. Search times dropped from 60 ms. The search method also has a limit to keep the auto-complete list is to a reasonable size. A '*' is appended to the string to support the general 'match' case.

string := aString asLowercase.
string last = $* ifFalse: [string := string copyWith: $*].
list := aCollection select: [:each | string match: each nameKey].
list size > limit ifTrue: [^list copyFrom: 1 to: limit].
^list

We match on #nameKey, but the displayed list uses #displayFullName, the mixed case version of the string.

OK, so the domain in as simple as it gets. The fun part was the Seaside code. JQAutocomplete>>search:labels:callback:  made it easy (props to its author). The method wraps a lot of complexity into a tight, useful package. In my case, the code looks like this...

html textInput
id: anId;
style: 'width: 300px; font-size: 14pt';
script: (html jQuery this autocomplete
search: [:string | self buildSearchResponse: aSearchBlock with: string]
labels: aLabelBlock
callback: [:value :script | self redirectToContact: value script: script])
#buildSearchResponseh:with: is a hack to show error messages in the auto-complete list. An exception answers an error object which is a subclass of the contact class, which is then displayed in the drop-down list.

^[aBlock value: aString]
on: Error
do: [:ex | Array with: (TSroloError newForErrorMessage: ex displayString)]

#aLabelBlock uses one of the three display methods for the drop down list (#displayFullName #displaySurname #displayCompany).

#redirectToContact:script: shows a nice URL with a ?contact=nnnn suffix.

| urlString |
urlString := aScript requestContext request url startingURL ,
'?contact=',
aContact displayContactIndex.
aScript goto: urlString.
self session unregister
In #renderContactOn: I check for the 'contact' parameter and render the contact details if found.

Here is what it looks like...
...typing 'bob n' into the full name fields shows...
...selecting 'Bob Nemec' then redirects to '?contact=3784', which shows the contact details...
...the '+' toggles a display of all the contact fields, using the jQuery toggle...
| moreId lessId |
moreId := html nextId.
lessId := html nextId.
html anchor
id: moreId;
onClick: (
html jQuery ajax script: [:s |
s << (html jQuery id: aComponentId) toggle: 0.3 seconds.
s << (html jQuery id: moreId) hide.
s << (html jQuery id: lessId) show]);
title: aMoreTitle;
with: [html image url: TSwaFileLibrary / #morePng].
html anchor
id: lessId;
style: 'display: none;';
onClick: (
html jQuery ajax script: [:s |
s << (html jQuery id: aComponentId) toggle: 0.5 seconds.
s << (html jQuery id: lessId) hide.
s << (html jQuery id: moreId) show]);
title: aLessTitle;
with: [html image url: TSwaFileLibrary / #lessPng]

Finally, reloading the list of contacts is done by adding a ?load parameter, which loads the contacts from a predefined file location. This way the contact load action does not need to be done by the application, but can be done by an OS scheduled task.

And again, a big thanks for the Seaside jQuery code which makes writing simple apps like this easy.

A bad day in [] is better than a good day in {}