JimmyG | Blog | Book | Life | Projects | Contact

YUI Autocomplete AJAX Select Drowdown with ID

Posted: Fri 19th Oct 2007, 9:42am
Tags: pylons, javascript, mako

The YUI toolkit comes with a very flexible autocomplete control but a common requirement is for an autocomplete control that submits the ID associated with a text value rather than the text value itself, much like a select field submits the option value, not the contents the user selects from a drop down list.

Luckily this is fairly easy to achieve using a forceSelection option, a hidden field, and a custom itemSelectEvent handler.

First setup the imports as described on the YUI AutoComplete page:

<!--CSS file (default YUI Sam Skin) -->
<link type="text/css" rel="stylesheet"
    href="http://yui.yahooapis.com/2.3.0/build/autocomplete/assets/skins/sam/autocomplete.css">

<!-- Dependencies -->
<script type="text/javascript"
    src="http://yui.yahooapis.com/2.3.0/build/yahoo-dom-event/yahoo-dom-event.js"></script>

<!-- OPTIONAL: Connection (required only if using XHR DataSource) -->
<script type="text/javascript"
    src="http://yui.yahooapis.com/2.3.0/build/connection/connection-min.js"></script>

<!-- OPTIONAL: Animation (required only if enabling animation) -->
<script type="text/javascript"
    src="http://yui.yahooapis.com/2.3.0/build/animation/animation-min.js"></script>

<!-- OPTIONAL: External JSON parser from http://www.json.org/ (enables JSON validation) -->
<script type="text/javascript" src="http://www.json.org/json.js"></script>

<!-- Source file -->
<script type="text/javascript"
    src="http://yui.yahooapis.com/2.3.0/build/autocomplete/autocomplete-min.js"></script>

Now let's set up the data structure in a Pylons controller action which the YUI component will access to populate the find as you type select dropdown. You could write similar code for Rails or PHP, it doesn't have to be Pylons. Notice that the @jsonify decorator converts the Python data structure we return to valid JSON:

@jsonify
def get_data(self):
    return {
        "ResultSet": {
            "totalResultsAvailable":"100",
            "totalResultsReturned":2,
            "firstResultPosition":1,
            "Result": [
                {
                    "ID": "897",
                    "Title":"foo",
                    "Summary":"... When foo' is used in connection with bar' it has generally traced...",
                    "Url":"http:\/\/www.catb.org\/~esr\/jargon\/html\/F\/foo.html",
                    "ModificationDate":1072684800,
                    "MimeType":"text\/html"
                },
                {
                    "ID": "492",
                    "Title":"Foo Fighters",
                    "Summary":"Official site with news, tour dates, discography, store, community, and more.",
                    "Url":"http:\/\/www.foofighters.com\/",
                    "ModificationDate":1138521600,
                    "MimeType":"text\/html"
                }
            ]
        }
    }

Now we need to write the code to generate the autocomplete control. YUI uses an existing text field as the autocomplete field and this will contain whatever text is looked up. Our application requires the corresponding ID so we need a hidden field to hold that value. The hidden field should have the name you want the ID to be submitted as, the text field can have any name because it doesn't contain the data the application needs. YUI also needs a container <div> which it populates with the results. Ours is called myContainer.

Here's some HTML to achive this:

<form action="/fayt/submit">

<p><label for="myInput">Sponsor Name<br /></label></p>

<div id="dashboard_autocomplete" style="clear:both; padding-bottom: 20px; width: 400px;">
    <input id="myInput_id" type="hidden" name="myInput_id" />
    <input id="myInput" type="text" name="item">
    <div id="myContainer"></div>
</div>

<input type="submit" name="submit" value="submit" />
</form>

Now let's add some JavaScript. This can go after the HTML above:

<script type="text/javascript">
    var mySchema = ["ResultSet.Result","Title","ID","Url","ModificationDate"];
    var myDataSource = new YAHOO.widget.DS_XHR("/fayt/get_data", mySchema);
    myDataSource.responseType = YAHOO.widget.DS_XHR.TYPE_JSON;
    var myAutoComp = new YAHOO.widget.AutoComplete("myInput","myContainer", myDataSource);
</script>

The first line sets up a schema, the first item of which specifies where the actual results are in the data structrue returned (in this case they are in the Result list of the ResultSet dictionary), the subsequent entries specify values within each result which might be displayed in the control.

Test this example by entering foo into the field. It works as expected returning two options and allowing you to select one but the hidden myInput_id field doesn't get populated.

To populate the hidden field we need to create a callback function and subscribe it to the autocomplete control's itemSelectEvent. Add the following code at the end of the JavaScript above, just before the </script> tag:

function fnCallback(e, args) {
    YAHOO.util.Dom.get("myInput_id").value = args[2][1];
 }
myAutoComp.itemSelectEvent.subscribe(fnCallback);

Now, when an item is selected from the autocomplete control, fnCallback gets called with two arguments. The first is an event object e and the second is a list of arguments described here. These are:

oSelf

<YAHOO.widget.AutoComplete> The AutoComplete instance.

elItem

<HTMLElement> The selected <li> element item.

oData

<Object> The data returned for the item, either as an object, or mapped from the schema into an array.

In this case we want to access the data returned for the item, which we can access as args[2]. Because of the way we set up mySchema earlier the ID field is the second item in the list (you don't count the first, "ResultSet.Result" because it isn't one of the data items). We can therefore access the ID of the selected item as args[2][1] (JavaScript arrays are counted from 0 so the second item is numbered 1). All that remains is to assign this ID to the field which is what the YAHOO.util.Dom.get("myInput_id").value = args[2][1]; line does.

Note

If you are adding this HTML and JavaScript to a Mako template in a Pylons application you can replace the URL "/fayt/get_data" with "${h.url_for(controller='fayt', action='get_data')}" and Pylons will generate the correct URL for you.

Now that the control is working properly let's write the code to get the ID in a Pylons controller action:

def submit(self):
    id = request.params.get('myInput_id')
    item = request.params.get('item')
    return "The submitted ID was: %s, the selected item was %s" % (id, item)

There are various options you can use to spice up the control. Try adding some of these at the end of the JavaScript, before the </script> tag:

myAutoComp.useShadow = true;
myAutoComp.forceSelection = true;
myAutoComp.useIFrame = true;

The second of these options ensures that a user selects one of the options from the list rather than entering any old value. This is essential if you want the autocomplete control to be able to replace an actual select control. The third puts the content in an iFrame so that on IE, any form elements beneath the div generated during the autocomplete do not show through.

You can also specify a function to change how the dropdown container formats the infomation. You could use this to display images, make certain parts of the text bold etc. Here's a simple example:

// This function puts the title in bold if the modification date is
// after 1100000000
myAutoComp.formatResult = function(aResultItem, sQuery) {
    var sKey = aResultItem[0]; // the entire result key

    // We aren't using these two in this example but it is useful
    // to know how to get them.
    var sKeyQuery = sKey.substr(0, sQuery.length); // the query itself
    var sKeyRemainder = sKey.substr(sQuery.length); // the rest of the result

    if (aResultItem[3] > 1100000000) {
        var aMarkup = [
           "<div id='ysearchresult'>",
           "<span style='font-weight:bold'>",
           sKey[0],
           "</span>",
           "</div>"
        ];
     } else {
       var aMarkup = [
           "<div id='ysearchresult'>",
           "<span style='font-weight:normal'>",
           sKey[0],
           "</span>",
           "</div>"
       ]
     }
     return (aMarkup.join(""));
};

That's it. You now have a fully customisable autocomplete control which can be used to replace ordinary HTML <select> fields in cases where there are too many items to easily list in a select alone.

If you spot any mistakes or have suggestions for improvements please feel free to leave a comment.