package ca.xty.myUtils {

    import flash.display.*;
    import flash.text.*;
    import flash.events.*;
    import flash.utils.*;
    import fl.controls.List;
    import fl.controls.Button;
    import fl.data.DataProvider;


The first thing you notice is that the package has a class path this time. This is because the AutoComplete.as class lives in a folder called myUtils, which in turn lives in a folder called xty, which itself lives in a folder called ca. Anytime you relocate this class to a different folder you must update the package's class path.

We import the usual suspects, plus the List and Button components and the DataProvider class.

public class AutoComplete extends Sprite {

        // These constant variables will be used in our event dispatches
        public static const SHOW_LIST:String = "SHOW_LIST";
        public static const MADE_CHOICE:String = "MADE_CHOICE";

        // These variables will be used to get information back to our Document Class which is why they have a public declaration
        public var aIndex:int;
        public var noResult:String;

        // These variables will hold the information contained in the parameters passed in the constructor
        private var _passedArray:Array;
        private var _txtWidth:int;
        private var _txtHeight:int;
        private var _whichProperty:String;
        private var _btnTxtColor:Number;

        // Display variables
        private var acList:List;
        private var acArray:Array;
        private var listHeight:int = 100;
        private var listHeightCalc:int;

        private var searchBtn:Button;
        private var sTerm:String;

        private var acTxt:TextField;

        // TextFormats
        private var titleFormat:TextFormat;
        private var topBtnFormat:TextFormat;

        // Convenience variables for loops
        private var i:uint;
        private var len:uint;


We create our public class AutoComplete and tell it to extend Sprite in order to use that classes functionality. The comments in the variables tell the story pretty well. One thing to note are the first two variables. They have been declared as public, and as such they will be able to travel between classes. The other thing to note, is that the variable listHeight is set to 100. This represents the default height for our List component. This will provide us with 5 visible rows before the scroll bars kick in. Change this number if you want your default List size smaller or bigger.

public function AutoComplete(PassedArray:Array, WhichProperty:String, TxtWidth:int, TxtHeight:int, BtnTxtColor:Number = 0x000000) {

            // Place the parameter values passed in the constructor into our private vars
            _passedArray = PassedArray;
            _txtWidth = TxtWidth;
            _txtHeight = TxtHeight;
            _whichProperty = WhichProperty;
            _btnTxtColor = BtnTxtColor;

            // Give our TextFormats some properties
            titleFormat = new TextFormat();
            titleFormat.size = 12;
            titleFormat.font = "verdana";
            titleFormat.leftMargin = 3;

            topBtnFormat = new TextFormat();
            topBtnFormat.color = _btnTxtColor;
            topBtnFormat.size = 10;
            topBtnFormat.font = "verdana";

            // Call the function to build the display
            buildAC();

    }


Our public function AutoComplete takes up to five parameters. The first four are mandatory and the last one is optional. It is optional because a default value has been set for the button text color. Inside the function, we grab the values passed in the constructor and apply them to this classes private variables. Next we give our TextFormats some properties. Note that the topBtnFormat uses the passed color parameter. Once all that is taken care of, we call the buildAC function.

private function buildAC():void{

            // This is the TextField users will type in what they want to search for
            // It takes it's width and height from the variables passed in the constructor
            // The event listener responds to any change in the TextField
            acTxt = new TextField();
            acTxt.x = 0;
            acTxt.y = 0;
            acTxt.width = _txtWidth;
            acTxt.height = _txtHeight;
            acTxt.type = "input";
            acTxt.border = true;
            acTxt.background = true;
            acTxt.backgroundColor = 0xffffff;
            acTxt.defaultTextFormat = titleFormat;
            acTxt.addEventListener(Event.CHANGE, updateDisplay);
            addChild(acTxt);

            // Our Search Button
            searchBtn = new Button();
            searchBtn.x = acTxt.width + 10;
            searchBtn.y = acTxt.y;
            searchBtn.width = 80;
            searchBtn.height = 20;
            searchBtn.label = "Search";
            searchBtn.setStyle("textFormat", topBtnFormat);
            searchBtn.addEventListener(MouseEvent.CLICK, searchHandler);
            addChild(searchBtn);

            // The List component that will display the auto-complete results
            // Notice it has the visibale property set to false
            // It's event listener responds to a change, ie an item is clicked
            acList = new List();
            acList.x = 0;
            acList.y = acTxt.y + acTxt.height;
            acList.setSize(_txtWidth, listHeight);
            acList.visible = false;
            acList.addEventListener(Event.CHANGE, listHandler);
            addChild(acList);

        }


The buildAC function is pretty self explanatory, especially with the comments left in. We are simply setting up the various bits and pieces that make up the visual part of this class. One thing to note here is the x and y positioning. I am not using my xPos and yPos vars because we have very little to set up, and because each piece is dependent on the position of the others. When you are thinking in terms of x and y inside the AutoComplete class do mistake that with the x and y placements on the stage. These internal x and y properties only affect the items inside the AutoComplete class. You always want the first item at x = 0, y = 0. That way you will be able to accurately judge where to put the instance of the AutoComplete on the main stage. So, our first item, the acTxt field is at 0,0. Now, since the acTxt field gets it's width from the parameters sent in the constructor we can never know for sure what that width will be. Our next item, the search button, has an x value of acTxt.width + 10, and a y value of acTxt.y. That way no matter how wide the acTxt field is, the search button will always be 10 pixels to the right. The acList uses an x value the same as the acTxt field, so it is simply 0. The y value of acList depends on the y value of acTxt plus the height of acTxt. We also want to make it the same width as the acTxt so in the setSize function width is set to our passed value _txtWidth and the height is set to our default value listHeight.

private function updateDisplay(e:Event):void{
  sTerm = acTxt.text;
        sTerm = sTerm.toLowerCase();
        acArray = new Array();
        len = _passedArray.length;

        for(i = 0; i < len; i++){
                var firstLabel:String = _passedArray[i][_whichProperty].toLowerCase();
                var firstLetter:String = firstLabel.substr(0, sTerm.length);
                if(firstLetter == sTerm){
                     acArray.push({label:_passedArray[i][_whichProperty], data:i});
                }
        }

     listHeightCalc = acArray.length * 20;
        if(listHeightCalc > listHeight){
                acList.height = listHeight;
        }else{
                acList.height = listHeightCalc;
        }

  acList.removeAll();
        acList.dataProvider = new DataProvider(acArray);
        acList.visible = true;
        dispatchEvent(new Event(AutoComplete.SHOW_LIST, true));
}


The updateDisplay function responds to any change taking place in the acTxt field. Each time a user enters a letter this function is fired. The first thing we do is set our sTerm variable to equal the value of the acTxt field. Then we set the sTerm to lower case. Next we set the acArray to be a new, empty array. Our len variable is set to the length of the _passedArray. Now we are all set to run a for loop and see if any of the values in the _passedArray match our sTerm.

We grab the value of the property _whichProperty from the first object in the array. Remember that _whichProperty equals the passed in value of "label". So, saying _passedArray[i][_whichProperty] is the same as saying _passedArray[i].label . Using the _whichProperty variable we can pass in any property name we choose. We call this variable firstLabel and set it to be lower case. Our next variable, firstLetter, is a string which is a sub-string of our firstLabel variable. Using the substr function we pass the parameter 0 to be our start index and the length of sTerm as the second parameter to indicate how many letters we should take. If you type in P it takes only the first letter of firstLabel, but if you type in PE it will take the first two, and so on.

Once we have established the firstLetter, we check that against the sTerm value. If they are the same, we create a new object and push that into our acArray. The object needs to have the property "label" so as to display something in List component. We use the current value of "i" to access the correct data and give our label property the value of _passedArray[i][_whichProperty], or _passedArray[i].label. The data property will be our index marker, and so once again we use the current value of "i" to mark our place in the _passedArray.

Once we have looped through all the contents of the _passedArray, we create our listHeightCalc variable by multiplying the acArray.length by 20. Now we compare this value to our default height variable, listHeight. If the listHeightCalc is greater than our listHeight, we set the height of the acList to the default, but if the listHeightCalc is less than the default,we set the acList to the lower value because we won't need the maximum number of rows to display the results of the search.

Now that the height of the acList is sorted out, we remove all contents and set the DataProvider to be the newly created acArray. Now that we have something to show, we set the acList's visible property to true, and dispatch the event that will make certain it is on the top of the display heap.

private function listHandler(e:Event):void{
        acTxt.text = e.target.selectedItem.label;
  aIndex = e.target.selectedItem.data;
        acList.visible = false;
        dispatchEvent(new Event(AutoComplete.MADE_CHOICE, true));
}


The listHandler function fires when an item in the list has been clicked on. This is registered as a CHANGE event. The first thing we do is set the acTxt field to have the value of the label property of the selected item. Then we set the aIndex variable to be the value of the selected item's data property. Now we set the acList's visible property to false, and dispatch the event that will tell the Document class that the user has made their choice.

private function searchHandler(e:MouseEvent):void{
        acArray = new Array();
        acList.visible = false;
        sTerm = acTxt.text;
        sTerm = sTerm.toLowerCase();

     for(i = 0; i < len; i++){
                var firstLabel:String = _passedArray[i][_whichProperty].toLowerCase();
                if(firstLabel.indexOf(sTerm) != -1){
                     acArray.push({label:_passedArray[i][_whichProperty], data:i});
                }
        }

     listHeightCalc = acArray.length * 20;
        if(listHeightCalc > listHeight){
                acList.height = listHeight;
        }else{
                acList.height = listHeightCalc;
        }

     if(acArray.length > 1){
                acList.removeAll();
                acList.dataProvider = new DataProvider(acArray);
                acList.visible = true;
                dispatchEvent(new Event(AutoComplete.SHOW_LIST, true));
        }else if(acArray.length == 1){
                acTxt.text = acArray[0].label;
                aIndex = acArray[0].data;
                dispatchEvent(new Event(AutoComplete.MADE_CHOICE, true));
        }else{
                aIndex = -1;
                noResult = "No Results for " + acTxt.text;
                dispatchEvent(new Event(AutoComplete.MADE_CHOICE, true));
        }
}


The searchHandler function fires when the search button is clicked, as you can tell by it's event type. This function follows pretty closely the same set of actions as the updateDisplay function. The difference comes in at the end. Whereas with the updateDisplay function we are going to show the acList component unless there are no results, with the searchHandler function we have a choice because the action has been initiated by the button click. By checking the length of the acArray we can decide what to do.

If the acArray's length is greater than 1, we will show the list by setting the acArray as the DataProvider after first running the removeAll function to clear out any previous contents, and then setting it's visible property to true.

If the acArray's length is equal to 1 then we have no need for the acList and we'll just put the single result directly into the acTxt field. Then we'll set the aIndex variable to equal the data property of the first, and only, object in the array, which is why we can use 0 as the index value. Now we can dispatch the event that will tell the Document class that a choice has been made.

If neither of the first two checks are true, then it stands to reason that we have no results to display. But we don't want to leave the user hanging, so we'll set our aIndex to -1 and the noResult string to equal our no results message plus the value of our acTxt field. Again we dispatch the choice has been made event so that our message will be properly displayed.

And that's it!

In Conclusion

As I said in the beginning, in those cases where you have half a dozen items to pick from you would use a ComboBox to display those choices because the AutoComplete would be overkill. But, if you have 50 or 60 or more items to choose from the AutoComplete can provide users with a quicker way to get where they are going. This version of AutoComplete is fairly simple in that it relies on you having a ready made array of objects to choose from. I can see a day when I will want to be able to query a database to put together the array of choices on the fly. Perhaps I will call the next version AutoCompleteLive. This current version will provide me with a solid set of core functionality to make the next version easier to create. So, let your imagination run free and have fun coding! As per usual, if you have questions don't hesitate to contact me.