Get Adobe Flash player

AutoComplete for Flash

Recently a client asked me if I could put together an AutoComplete feature as part of the Content Management System I am building for them using AS3. Google has made this feature very popular in recent times, and so I thought it would be an interesting exercise to do in Flash. First I poked around a bit to see who had done what with this concept and was surprised to find very little information. What I did find pertained to Flex, and while I also use Flex, I wanted to do this in pure AS3. So, I put on the coffee, sat down and thought out how to go about it. Read on for the results.

The Demo

This demo uses a hard coded array of objects through which the user will search for their favorite fruits. In the real world application for my client, the array is gathered from a database. To see how it works, type in the letter P. It is not case sensitive as you will see when we dissect the code. You get a list with four fruits that start with the letter P. Now, type in an E, so that you have PE. The list of four reduces itself to a list of two. At anytime you can select one of the choices from the list and the information for that fruit will be displayed below. So, what is the Search button for you might be thinking? Well, try this. Suppose you want to search for melons. Type in the the letter M. None of the results displayed in the List is Melon. Type in the next letter E, and the List disappears. You might be thinking that there are no melons available, but continue typing until you have the entire word melon in the search field. Now click Search. You will see two results in the list that were not available before. We'll be getting into all the gory details shortly.

The Demo Files

Download the autoComplete.zip. Included in this file is the autoCompleteDemo.fla, the AutoDemo.as, and the AutoComplete.as. The AutoComplete.as class is found in the folder system ca.xty.myUtils. If you have your own class library, you can move this class into the appropriate folder - just remember to change the package details in the class to match your folder system.

The files are made in Flash CS5, but if you are using CS4 do not run off! The AS class files do not care what version of Flash you have. In order to use these files all you will need to do is make yourself a new fla. The fla you create can be called anything you want. Set it's width to 500 and it's height to 300. Put a copy of the List and Button components in the library and set the Document Class to AutoDemo. Make sure all the files are in the same folder and you are good to go.

In the autoCompleteDemo.fla you will see that there is nothing on stage. Looking in the library, you will see that we have included a Button component and a List component. Also notice that the Document Class is set to AutoDemo - no .as extension. And that is it for the fla. Next, let's take a look at the Document Class, AutoDemo.as.

Document Class

The Document Class is what runs the show. It gathers all other needed classes in it's import statements and sets up the display area. Let's start right at the top.

    package  {

The package statement is very simple because this class is in the same folder as the fla.

    import flash.events.*;
    import flash.text.*;
    import flash.display.*;
    import flash.geom.*;
    import ca.xty.myUtils.AutoComplete;

Below that we import the goodies we need. The asterisk is a wild card, which indicates that we are importing all the classes from a certain package. This is fine since the complier only includes the actual classes it needs when you publish the fla. Just to show you how you can target one particular class, the last import statement only gets the AutoComplete class from the package system ca.xty.myUtils.

    public class AutoDemo extends Sprite{
        private var ac:AutoComplete;
        private var fruitArray:Array = [{label:"Apple", Type:"Pome", Description:"The fruit is developed from a compound inferior ovary. The ripened tissue around the ovary forms the fleshy edible part."}...];

        private var bg:Sprite;

        private var t1:TextField;
        private var t2:TextField;
        private var it1:TextField;

        private var titleFormat:TextFormat;
        private var titleFormatW:TextFormat;

        private var xPos:int;
        private var yPos:int;

Now we declare our class AutoDemo and set it to extend the Sprite class. Next we create our variables.

The first variable is an instance of our AutoComplete class called ac.

Next we have our array, which as I said earlier is hard coded here for convenience. Here I am only showing enough for you to note the structure of it. It is an array of Objects, with each object containing the following properties: {label:"Apple", Type:"Pome", Description:"The fruit..."} The curly braces declare that this is an Object. The name/value pairs are familiar if you have created such objects for the ComboBox or List components.

Then we have a variable which is a Sprite and will serve as our header background.

After that, we have 3 TextFields, and two TextFormats.

Lastly we have two variables which we will use to hold our x and y positions. Using these it is easy to move things around in a block. Suppose that for some reason you decide you want the whole works to be set at 10 pixels lower. With the xPos, yPos system on place all you have to do is change the first yPos from 0 to 10 and everything shifts downward. In a set up with dozens of display objects on the stage this can be a great time saver.

    public function AutoDemo() {

This sets up our constructor function for the AutoDemo class.

    addEventListener(AutoComplete.MADE_CHOICE, setDisplay);

    titleFormat = new TextFormat();
    titleFormat.color = 0x000000;
    titleFormat.size = 12;
    titleFormat.font = "verdana";
    titleFormat.align = "left";

    titleFormatW = new TextFormat();
    titleFormatW.color = 0xffffff;
    titleFormatW.size = 12;
    titleFormatW.font = "verdana";
    titleFormatW.align = "left";

    xPos = 0;
    yPos = 0;

    bg = createSprite(0x004083, 500, 60, xPos, yPos);
    addChild(bg);

    xPos += 20;
    yPos += 5;

    t1 = new TextField();
    t1.x = xPos;
    t1.y = yPos;
    t1.width = 200;
    t1.height = 20;
    t1.text = "Look up your favorite Fruit";
    t1.setTextFormat(titleFormatW);
    addChild(t1);

    yPos += 25;

    t2 = new TextField();
    t2.x = xPos;
    t2.y = yPos;
    t2.width = 50;
    t2.height = 20;
    t2.text = "Search:";
    t2.setTextFormat(titleFormatW);
    addChild(t2);

    xPos += 55;

    ac = new AutoComplete(fruitArray, "label", 150, 20, 0xffffff);
    ac.x = xPos;
    ac.y = yPos;
    ac.addEventListener(AutoComplete.SHOW_LIST, listDepth);
    addChild(ac);

    xPos += 25;
    yPos += 35;

    it1 = new TextField();
    it1.x = xPos;
    it1.y = yPos;
    it1.width = 300;
    it1.height = 80;
    it1.type = "input";
    it1.multiline = true;
    it1.wordWrap = true;
    it1.autoSize = TextFieldAutoSize.LEFT;
    addChild(it1);
}

The first thing we do inside the constructor is set up an event listener. This will listen for an event which is dispatched from the AutoComplete class once a user has made a choice. We'll look a little closer at this when we get to the setDisplay function.

Next we give our TextFormats a few properties.

Now we set our initial xPos and yPos vars to 0.

Then we draw our header background using the createSprite function, which is described further down.

With our header in place, we adjust the xPos by 20 and the yPos by 5. This sets up the placement of our first TextField, t1, which is just a title field. Since the header background is a dark blue we use the TextFormat titleFormatW, which uses white as the text color.

We want the next TextField, t2, right underneath t1, so we simply adjust the yPos by 25 and leave the xPos alone. Now we add in t2 and adjust our xPos by the t2 width plus 5 for a little gap, because we want our instance of AutoComplete to line up right beside the t2 field.

Our variable ac gives birth to a new instance of AutoComplete, and passes in some property values via the constructor. The first parameter passed is the array we are using. The next parameter is the name of the property within the object whose value we want to search for. Sending this as a parameter gives you additional flexibility. Depending upon the nature of your information, you may have properties within your array objects such Name or Description. If you want to use these values to search in, just pass them as a string, "Name". The "label" is probably the most common property you will use, but not necessarily the only one. The next two properties are the width and height of the search TextField you want to create. The last property is defined as optional because in the AutoComplete class this property is set with a default value. This number represents the search button text color. The default is black, so, because our header is dark blue, we will set this properties' value to white.

Then we position our instance on stage using the xPos and yPos values.

The event listener waits to hear from the AutoComplete class that it needs to adjust the depth of the instance in order to show the List above everything else on stage. The way the display list works, each object is added one after another starting at the bottom. In our case the it1 TextField is at a higher depth than our ac instance. So, when the ac instance makes the List component visible it would be underneath the it1 TextField. The listDepth function takes care of this problem each time this event is fired. You could simply add the it1 TextField before adding the ac instance, but suppose you have display objects that are set up dynamically once the user makes a choice? For instance, in the project I made the AutoComplete for, a series of buttons are added to the stage once the user picks something from the search field. These buttons will always be put on stage at a higher depth than the instance of the AutoComplete.

I wanted to center the it1 TextField so I added 25 to the xPos and 35 to the yPos to bring it below the header background.

The it1 TextField has the multiline and wordWrap properties set to true, so that by using an autosize value of LEFT we can force the TextField to expand down if we encounter more text than we have room for.

private function listDepth(e:Event):void{
    setChildIndex(ac, numChildren - 1);
}

Here is our listDepth function which is called when an event is dispatched from the AutoComplete class telling us basically that the List component is now going to be visible so you had better make sure it is on top. We do this using the setChildIndex, which takes two parameters: the child to be re-indexed, and the depth at which we want it to be placed. In our case we want this depth to be the highest we can get. We take the total number of children on the stage and subtract one from that integer. We subtract the 1 because the display list, like an array, is zero based. So, if numChildren equals 3, then the occupied depths are 0, 1, 2. The numChildren - 1 gives us the top position. Everything else on the stage is simply pushed down.

private function setDisplay(e:Event):void{
    var fIndex:int = ac.aIndex;
    if(fIndex == -1){
        it1.text = ac.noResult;
        it1.setTextFormat(titleFormat);
    }else{
        it1.htmlText = "Fruit Type: " + fruitArray[fIndex].Type + "

Description: " + fruitArray[fIndex].Description; it1.setTextFormat(titleFormat); } }

The setDisplay function is also fired through an event dispatched in the AutoComplete class. This time the event is telling us that a choice has been made by the user, please display it now.

In the AutoComplete class we have a public variable called aIndex. Because it is public we are able to pull it into our Document class by setting the fIndex variable to the value of the ac.aIndex variable. If fIndex is equal to -1, then we have no result to display, and so we set our it1 Textfield to read the noResult message from the instance ac by using the same dot syntax which gave us access to the aIndex variable. However, if fIndex does not equal -1, then we have something to display. The fIndex variable represents the index number of the chosen item from our fruitArray. All we need to do to get the right information is pull it out of the fruitArray using this index. So, we are going to set it1's text field to accept html text and then put together the string. From our fruitArray we pick the object in the fIndex position within the array and grab the property Type, and then the property Description.

private function createSprite(color:int, w:int, h:int, x:int, y:int):Sprite{
    var s:Sprite = new Sprite();
    s.graphics.beginFill(color);
    s.graphics.drawRect(0, 0, w, h);
    s.graphics.endFill();
    s.x = x;
    s.y = y;
    return s;
}

The createSprite function draws a simple rectangle for us. The parameters are, color, width, height, x position and y position. The graphics class uses this information to draw a rectangle and returns us a nice shiny Sprite.

And that is it for our Document Class. Now let's jump into the AutoComplete class itself.

AutoComplete.as

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 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 _whichIndex: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 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, WhichIndex: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;
    _whichIndex = WhichIndex;
    _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 dependant 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][_whichIndex].toLowerCase();
        var firstLetter:String = firstLabel.substr(0, sTerm.length);
        if(firstLetter == sTerm){
            acArray.push({label:_passedArray[i][_whichIndex], 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 _whichIndex in from the first object in the array. Remember that _whichIndex equals the passed in value of "label". So, saying _passedArray[i][_whichIndex] is the same as saying _passedArray[i].label . Using the _whichIndex 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][_whichIndex], 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 visble 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][_whichIndex].toLowerCase();
        if(firstLabel.indexOf(sTerm) != -1){
            acArray.push({label:_passedArray[i][_whichIndex], 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

In most cases where you have half a dozen items to pick from you would use a ComboBox to display those choices, and the AutoComplete would be overkill. But, if you have 50 or 60 items to choose from the AutoComplete could come in handy. This version of AutoComplete is fairly simple in that it relies on you having a ready made array of objects to choose. I can see a day when I will want to be able to query a database to put together the array of choices. 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 always, if you have any questions, don't be shy. Use the comment form below, or drop me a line./p>

Get Adobe Flash player

Comments

No Comments