Flex Type Ahead List Boxes

When a user types a character the default behaviour of a list box is to jump to the first item that starts with that character. This is fine for small lists but for a long list of items it's not very useful. A better solution would be if the list box remembered what the user typed and keep looking for matches on the second character, the third character, etc etc.

Here how you can do this via an Actionscript class that extends the List Flex class.

package
{
   import mx.controls.List;
   import flash.events.KeyboardEvent;
   import flash.events.FocusEvent;

   public class TypeList extends List
   {
      public function TypeList()
      {
         super();
         
         // set up event handlers
         this.addEventListener(KeyboardEvent.KEY_DOWN, jumpToItem);
         this.addEventListener(FocusEvent.FOCUS_IN, clear);
         this.addEventListener(FocusEvent.FOCUS_OUT, clear);
      }
      
      private var userTyped:String = "";
      
      //clear memory of what user typed
      private function clear(event:FocusEvent):void {
         userTyped = "";
      }
      
      // Stop default behaviour
      protected override function findKey(eventCode:int):Boolean {
         return false;
      }
      
      // Try and find an item that matches what the user types if it matches jump to it
      private function jumpToItem(event:KeyboardEvent):void {
         
         // ignore any out of range characters
         if (event.keyCode < 33 || event.keyCode > 126) {
return;
}

         // build up string of what user typed
         userTyped += String.fromCharCode(event.keyCode);
         
         var length:Number = this.dataProvider.length;
         var found:Boolean = false;
         var start:Number = 0;
         var match:String = userTyped.toLowerCase();
         
         // start at the selected index
         if (this.selectedIndex > 0) {
            start = selectedIndex;
         }
         
         for (var i:Number = start; i < length; i++) {
            var item:String = '';

            if (this.labelFunction != null) {
               item = this.labelFunction(this.dataProvider[i]).toLowerCase();
            }
            else if (this.labelField) {
               item = this.dataProvider[i][this.labelField].toLowerCase();
            }
                                 
            if (item.indexOf(match) == 0) {
               // set the selected index and scroll to it
               this.selectedIndex = i;
               this.scrollToIndex(i);
               found = true;
               return;
            }            
         }
         
         // if not found start again at the beginning
         for (i = 0; i < start; i++) {
            if (this.labelFunction != null) {
               item = this.labelFunction(this.dataProvider[i]).toLowerCase();
            }
            else if (this.labelField) {
               item = this.dataProvider[i][this.labelField].toLowerCase();
            }
                     
            if (item.indexOf(match) == 0) {
               // set the selected index and scroll to it
               this.selectedIndex = i;
               this.scrollToIndex(i);
               found = true;
               return;
            }            
         }
                  
         // if not found clear what the user has typed
         if (!found) {
            userTyped = "";
         }
         
         return;
      }      
      
   }
   
}

Two things to notice:

  • You need to override the findKey method of the List class for this to work.
  • The code looks at both the labelFunction and labelField to match with what the user has typed.

And here' an MXML file using the above Actionscript class.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:local="*" layout="vertical" creationComplete="init()" viewSourceURL="srcview/index.html">
   <mx:Form>
      <mx:FormHeading label="Select Suburb" />                     
      <mx:FormItem label="Suburbs">
         <local:TypeList dataProvider="{locations}" rowCount="10" width="300" labelFunction="locationPostcode" />
      </mx:FormItem>
   </mx:Form>
   
   <mx:Script>
      <![CDATA[
         import mx.collections.*;
         
         [Bindable] private var locations:ArrayCollection = new ArrayCollection();
         
         private var loader:URLLoader;
         
         public function init():void {
            // load postcode csv file
            this.loader = new URLLoader();
            this.loader.dataFormat = "text";
            this.loader.addEventListener("complete",this.loaded);
            this.loader.load(new URLRequest("postcodes.csv"));
            
            // sort by location
            var sort:Sort = new Sort();
            sort.fields = [new SortField("location")];
            this.locations.sort = sort;
            this.locations.refresh();
         }
         
         // called when postcode file loaded
         private function loaded(evt:Event):void {
            var file:String = this.loader.data;
            var lines:Array = file.split('\r');
            
            // covert file into an array collection
            for (var i:Number = 1; i < lines.length; i++)
            {
               var data:Array = lines[i].split(',');
               var item:Object = new Object();
               
               item.postcode = data[0];
               item.location = data[1];
               
               this.locations.addItem(item);   
            }
         }         
         
         // display suburb and postcode
         private function locationPostcode(data:Object):String {
            return data.location + " (" + data.postcode +")";
         }
      ]]>
   </mx:Script>
</mx:Application>

This may not be the most efficient way of matching items as it looks through the entire list to find a match rather than just looking through a sublist once more than once character is typed. However it does make for simpler code and works quickly for large lists. If you had 50,000+ items in the list you might want to do it another way.

Here's the running sample:

Related Blog Entries

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Copyright © Justin Mclean 2008
BlogCFC by Raymond Camden.