Previous Section Table of Contents Next Section

Hack 44 Store a List of All Input Words

figs/expert.gif figs/hack44.gif

Accumulate a list of popular words for the purposes of searching, indexing, or text autocomplete and store it in a local file.

We saw earlier how to autocomplete text [Hack #43] from a preinitialized word list (i.e., a limited dictionary). That works fine for common words, but in the real world, the definition of "common" can vary by situation. For example, the 300 or 3,000 most common words when talking about computers are not the same words used in common conversation (even if all your friends are geeks, they weren't the sample population used to determine the words in the preceding dictionary).

And the most common use for autocompleting text fields is when filling out online forms. These tend to be populated with uncommon words specific to the end user, such as her name and the name of her street and city.

So this hack shows how to dynamically build a list of words based on those that have been entered previously (which by definition means they're popular in your neck of the woods).

Add New Words to the Dictionary

We want to consider looking for new words only if the current text input has been completed by the user. On a form, this would occur when the user clicks a Submit button to indicate she has finished.

As part of the onRelease( ) event handler of this Submit button, we can search the text entered in our text field(s) and add to our dictionary any words not already in it.

The Code

The following code achieves this (assuming the code in [Hack #43] is also present and that the Submit button's onRelease( ) event handler invokes the function entered( )).

function entered( ) {

  var newWords:Array = new Array( );

  newWords = myText_txt.text.split(" ");

  for (var i = 0; i < newWords.length; i++) {

    if (myText.indexOf(newWords[i].toString( )) == -1) {

      // New word not in dictionary,

      // so add it and re-sort the dictionary.

      myText += " " + newWords[i];

      dictionary.push(newWords[i]);

      dictionary.sort( );

  }

}

Lines 2 and 3 of this function create a new array, newWords, consisting of all the words in the text field myText_text. If you had multiple text fields in your form, you could simply concatenate them all into one string with something like this instead of the current line 3:

newWords = (myTextField1_txt.text + " " +

            myTextField2_txt.text).split(" ");

The for loop's body (lines 4-11) searches the dictionary for every word in the text field (you could, of course, limit it to words with three or more letters). If the new word is not found, it is added to the dictionary, and the dictionary is re-sorted.

Although this code uses the text in the string myText for a fast search [Hack #79], it uses the dictionary array to store new words [Hack #43] . You can see this in action if you test the movie in Debug Movie mode (ControlDebug Movie) by looking at the array _root.dictionary as you enter "aardvark." When you click the Submit button, you will see that dictionary[0] now stores the word "aardvark." If you backspace until you have deleted "aardvark" and then start to retype it, Flash autocompletes the word once you enter "aa".

Save the Dictionary for Later Use

Storing the words in Flash variables is great, but the new words will be lost every time the user exits the form. We need to store the dictionary between sessions locally on the user's hard drive. The easiest way to do this is to save the variable myText, which is a string that holds the entire dictionary (including both default and new words).

The following listing shows the code needed to search for new words and store them locally (additions from earlier versions are shown in bold):

function entered( ) {

  var newWords:Array = new Array( );

  var needUpdate:Boolean = false;

  newWords = myText_txt.text.split(" ");

  for (var i = 0; i < newWords.length; i++) {

    if (myText.indexOf(newWords[i].toString( )) == -1) {

      // New word not in dictionary, 

      // so add it and re-sort the dictionary.

      myText += " " + newWords[i];

      dictionary.push(newWords[i]);

      dictionary.sort( );

      needUpdate = true;

    }

  }

  if (needUpdate) {

    saveDictionary( );

  }

}

function saveDictionary( ) {

  var shared:SharedObject = SharedObject.getLocal("dictionary");

  shared.data.dictionary = myText;

  shared.flush( );

}

function loadDictionary( ) {

  var shared:SharedObject = SharedObject.getLocal("dictionary");

  if (shared.data.dictionary != undefined) {

    myText = shared.data.dictionary;

  }

}

function enterEvent( ) {

  entered( );

  // Do other Submit button stuff...

}

function autoComplete( ) {

  if (Key.isDown(Key.CONTROL)) {

    myText_txt.text = complete_txt.text+" ";

    Selection.setSelection(myText_txt.text.length, 

                           myText_txt.text.length);

  }

}

function fieldChange( ) {

  match = "";

  startOfWord = this.text.lastIndexOf(" ") + 1;

  lastWord = this.text.substring(startOfWord, this.text.length);

  if (lastWord.length>1) {

    for (var i = 0; i < dictionary.length; i++) {

      if (lastWord == (dictionary[i].substr(0, lastWord.length))) {

        // match found

        match = dictionary[i];

        search = i;

        break;

      }

    }

  } else {

    search = 0;

  }

  complete_txt.text = this.text.substr(0, startOfWord) + match;

}

// Initialize

// Full list of words not shown

var myText:String = "about above... you young your";

var dictionary:Array = new Array( );

var search:Number = 0;

var lastWord:String = "";

var startOfWord:String = "";

var control:Object = new Object( );

// Load stored dictionary

loadDictionary( );

// Set up dictionary

dictionary = myText.split(" ");

dictionary.sort( );

// Set up events and listeners

myText_txt.onChanged = fieldChange;

control.onKeyDown = autoComplete;

Key.addListener(control);

enter_btn.onRelease = enterEvent;

The third line of the revised entered( ) function defines a Boolean, needUpdate, that is set to true when new words are found that need to be added to the dictionary. The last line of entered( ) calls saveDictionary( ) if new words have been added to the dictionary. The saveDictionary( ) function saves the text contained in the current full dictionary as a local shared object (LSO). An LSO is just a convenient way to store data between sessions akin to a browser cookie. We'll load the data back in when we need it.

When you attempt to store an LSO that exceeds the allowable size on a user's machine, the Flash Player requests the user's permission to store more data by displaying the Local Storage tab of the Settings dialog box. By default, the local storage limit is 100 KB per domain.

If you intend to store data locally, advise the user of its purpose and intended use. If he sees the dialog box appear unexpectedly, he may get very jittery, especially if it appears when filling out a form to purchase something online!

Loading the saved dictionary is accomplished by loadDictionary( ). This looks for a previously saved dictionary in the LSO and, if it exists, the new text is used to replace the default dictionary, myText.

You can test that new words are now persistent between separate sessions by testing the movie and entering "aardvark" in the text field, then closing the SWF file and running it again. The second time, you will see that "aardvark" autocompletes after the second letter.

Final Thoughts

Notice that the final listing searches the dictionary in two ways:

  • When the user is typing words in the text field, we search alphabetically through the dictionary array for the purposes of autocompletion.

  • When we want to see if any of the newly entered words are in the dictionary, we perform a search using the myText string.

Thus, we have two versions of the list of words, one that allows a structured alphabetical search and one that allows us to say if a newly input word is in our current dictionary. Both searches are optimized for speed.

Also, you may have noticed that we've assumed all words in the word list have equal popularity, or rather that their popularity matches their alphabetical order. That isn't necessarily true. For example, the word "said" is more popular than "sad" but if we arrange our dictionary alphabetically, "sad" will be suggested before "said." One solution is to list terms in order of popularity (either by using the popularity count provided by large statistical analyses or by simply counting their usage in relation to the current application). But this would slow down searching if the list isn't also (at least partially) alphabetical. If the word lists are small, searching should always be relatively fast. If the word lists are large, then the appropriate solution depends on the degree to which the alphabetical word order is counterproductive.

This technique of capturing popular words can be used for more than autocompleting text. It can be used to accumulate statistics to, say, track the most popular answer to a survey question or to suggest popular search terms when no matches can be found for the requested search term. Of course, those more sophisticated uses apply primarily when you are capturing data on the back end server. This hack captures data locally, assuming that each user's commonly entered words are specific to her (such as her name and street address). You could use Flash Remoting or other approaches to upload the data to a server and accumulate statistics across multiple users.

    Previous Section Table of Contents Next Section