Building AutoComplete Inputs with PHP, PEAR, Dojo and YUI

February 4, 2009

Tutorials

Building AutoComplete Inputs with PHP, PEAR, Dojo and YUI

Fashion Statements

Coco Chanel, the famous French designer, once commented that fashion “is not something that exists in dresses only”. In recent months, I’ve come to agree: ever since the big search engines added auto-suggest, hardly a week goes by without a client asking me to build similar autocomplete functionality into a Web form.

Fortunately, modern programming toolkits like Dojo provide ready-made widgets that have the necessary client-side functions for autocomplete. Add a little bit of server-side glue, in the form of a PHP script that talks to a database to generate valid suggestions, and enabling this functionality in a Web application now becomes a matter of hours, rather than days. In this article, I’ll show you how to do this using three different libraries: PEAR HTML_QuickForm, YUI, and Dojo. Come on in, and find out more!

Back To Basics

Before we get started, it’s important to state the assumptions this article makes. While most of the code in this tutorial involves the use of ready-made widgets, you’ll still find it easier to navigate if you understand HTML, know the basics of JavaScript and DOM programming, and are familiar with retrieving and processing SQL result sets in PHP.

This article also assumes that you have an Apache/PHP/MySQL development environment already set up, and that you have downloaded and successfully installed the following packages:

  1. The Dojo/Dijit Toolkit, which is available from http://www.dojotoolkit.org/. This article uses Dojo v1.2.3.
  2. The Yahoo! User Interface (YUI) Library, which is available from http://developer.yahoo.com/yui/. This article uses YUI v2.6.0.
  3. The HTML_QuickForm package and all necessary dependencies, which is available from http://pear.php.net/package/HTML_QuickForm, and is currently maintained by Bertrand Mansion and Alexey Borzov. This article uses HTML_QuickForm v3.2.9

A quick word also about the MySQL database used in this article. The various examples that follow demonstrate autocomplete functionality in the context of a Web form that accepts book information, consisting of the book title and up to three author names. Based on the first few characters entered into the form, author names are dynamically suggested from a database table, which looks something like this:

mysql> SELECT * FROM author LIMIT 0,10;
+----------+--------------------+
| AuthorID | AuthorName         |
+----------+--------------------+
|        1 | Stephen King       |
|        2 | Alfred Hitchcock   |
|        3 | Enid Blyton        |
|        4 | Ken Follett        |
|        5 | W. G. Moore        |
|        6 | Jeffrey Arthur     |
|        7 | John le Carre      |
|        8 | Scott Smith        |
|        9 | Alison Prince      |
|       10 | Arthur Conan Doyle |
+----------+--------------------+
10 rows in set (0.13 sec)

An SQL file containing example data for this table can be downloaded here.

Remote Control

Once all the pieces are in place, let’s look at one approach to enabling autocomplete functionality: using YUI’s AutoComplete widget. This widget, which is documented in detail at http://developer.yahoo.com/yui/autocomplete/, can be associated with any form input element and is a fully-skinnable, easy-to-configure component that can read suggestion data in a variety of formats.

Here’s an example of a form that uses the AutoComplete widget:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
  <link rel="stylesheet" type="text/css" href="/yui/build/fonts/fonts-min.css" />
  <link rel="stylesheet" type="text/css" href="/yui/build/autocomplete/assets/skins/sam/autocomplete.css" />
  <script type="text/javascript" src="/yui/build/yahoo-dom-event/yahoo-dom-event.js"></script>
  <script type="text/javascript" src="/yui/build/connection/connection-min.js"></script>
  <script type="text/javascript" src="/yui/build/animation/animation-min.js"></script>  
  <script type="text/javascript" src="/yui/build/datasource/datasource-min.js"></script>
  <script type="text/javascript" src="/yui/build/autocomplete/autocomplete-min.js"></script>
  </head>
  <body class="yui-skin-sam">
    
    <h3>Add Title</h3>
    <form action="" method="post">
      Title:<br/>
      <input type="text" name="Title[TitleName]" style="width:200px"/>
      <p/>
      Author:<br/>
      <div style="width:200px">
        <input id="Author[AuthorName][0]" type="text" name="Author[AuthorName][0]"/>
        <div id="ac0"></div>
      </div>
      <p/><br/><p/>
      Author:<br/>
      <div style="width:200px">
        <input id="Author[AuthorName][1]" type="text" name="Author[AuthorName][1]"/>
        <div id="ac1"></div>
      </div>
      <p/><br/><p/>
      Author:<br/>
      <div style="width:200px">
        <input id="Author[AuthorName][2]" type="text" name="Author[AuthorName][2]"/>
        <div id="ac2"></div>
      </div>
      <p/><br/><p/>
      <input type="submit" name="submit" value="Save" />
    </form>    
    
    <script type="text/javascript">                
      YAHOO.example.autocomplete = function() {
        var oConfigs = {
            prehighlightClassName: "yui-ac-prehighlight",
            queryDelay: 0,
            minQueryLength: 0,
            animVert: .01,
        }
        
        // instantiate remote data source
        // code based on example at 
        // http://developer.yahoo.com/yui/examples/autocomplete/ac_basic_xhr.html
        var oDS = new YAHOO.util.XHRDataSource("http://localhost/get_authors.php"); 
        oDS.responseType = YAHOO.util.XHRDataSource.TYPE_XML; 
        oDS.responseSchema = { 
           resultNode: 'author', 
           fields: ['name']             
        }; 
        oDS.maxCacheEntries = 10;         
    
        // instantiate YUI autocomplete widgets
        var oAC0 = new YAHOO.widget.AutoComplete("Author[AuthorName][0]", "ac0", oDS, oConfigs);          
        var oAC1 = new YAHOO.widget.AutoComplete("Author[AuthorName][1]", "ac1", oDS, oConfigs);          
        var oAC2 = new YAHOO.widget.AutoComplete("Author[AuthorName][2]", "ac2", oDS, oConfigs);
        return {
            oDS: oDS,
            oAC0: oAC0,
            oAC1: oAC1,
            oAC2: oAC2
        };
      }();
     </script>
    
  </body>
</html>

As the code illustrates, this form contains three input fields, each of which is associated with an AutoComplete widget. As the user begins entering data into each of these fields, the AutoComplete widget will go to work generating suggestions that match the user’s input. The source data for these suggestions is YUI’s XHRDataSource widget, which is configured as below:

        // instantiate remote data source
        var oDS = new YAHOO.util.XHRDataSource("http://localhost/get_authors.php"); 
        oDS.responseType = YAHOO.util.XHRDataSource.TYPE_XML; 
        oDS.responseSchema = { 
           resultNode: 'author', 
           fields: ['name']             
        }; 

This configuration tells the XHRDataSource widget that it should automatically poll the PHP script at ‘http://localhost/get_authors.php’ for suggestions, and warns it that the PHP script will express these suggestions in XML. The configuration also specifies the XML node and attribute names that contain the suggestion text.

At the other end of the connection, here’s what the ‘get_authors.php’ script looks like:

<?php
// begin XML output
$xmlStr = <<<XML
<?xml version='1.0' standalone='yes'?>
<authors>
XML;

// open database connection
$mysqli = new mysqli("localhost", "user", "pass", "library");      
if (mysqli_connect_errno()) {
    printf("Connect failed: %s
", mysqli_connect_error());
    exit();
}

// retrieve author list matching input
// add to XML document
$q = $mysqli->real_escape_string($_GET['query']);
$sql = "SELECT AuthorName FROM author WHERE AuthorName LIKE '" . $q . "%' ORDER by AuthorName";
if ($result = $mysqli->query($sql)) {
  while ($row = $result->fetch_row()) {
    $xmlStr .= '<author name="' . $row[0] . '"></author>';
  }
  $result->close();
}

// clean up
// output XML document
$mysqli->close();
$xmlStr .= '</authors>';
header("Content-Type: text/xml");
echo $xmlStr;
?>

There’s nothing particularly complicated about this script. It receives the string fragment entered by the user in $_GET['query'], formulates an SQL query to generate author names matching this fragment, and outputs an XML document containing these author names. Here’s an example of the XML it generates in response to a query for ‘ste’:

<?xml version='1.0' standalone='yes'?>
<authors>
  <author name="Stephen Fry"></author>
  <author name="Stephen King"></author>
  <author name="Stephen Mogridge"></author>
</authors>

At the other end of the connection, this XML is parsed and decoded by YUI’s XHRDataSource widget and presented to the user. Here’s an example of what the output looks like:

Going Local

One important point to note about the previous example, is that it continually “polls” the server to refine the suggestion list, as the user is typing. This might not be ideal in all situations – for example, when you’re on a slow network link, or if your server is already heavily loaded and you don’t want to unnecessarily burden it with additional work.

In these situations, you can populate the AutoComplete widget from a local data source – an inline JavaScript array – instead of from a remote URL. In this case, the array is dynamically generated using PHP when the page is rendered and used as the base for all autocomplete suggestions. The PHP code used to generate and populate the JavaScript array is similar to that used in the previous example. Here’s the revised form:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
  <link rel="stylesheet" type="text/css" href="/yui/build/fonts/fonts-min.css" />
  <link rel="stylesheet" type="text/css" href="/yui/build/autocomplete/assets/skins/sam/autocomplete.css" />
  <script type="text/javascript" src="/yui/build/yahoo-dom-event/yahoo-dom-event.js"></script>
  <script type="text/javascript" src="/yui/build/animation/animation-min.js"></script>  
  <script type="text/javascript" src="/yui/build/datasource/datasource-min.js"></script>
  <script type="text/javascript" src="/yui/build/autocomplete/autocomplete-min.js"></script>
  </head>
  <body class="yui-skin-sam">
    
    <h3>Add Title</h3>
    <form action="" method="post">
      Title:<br/>
      <div style="width:200px">
        <input type="text" name="Title[TitleName]"/>
      </div>
      <p/>
      Author:<br/>
      <div style="width:200px">
        <input id="Author[AuthorName][0]" type="text" name="Author[AuthorName][0]"/>
        <div id="ac0"></div>
      </div>
      <p/><br/><p/>
      Author:<br/>
      <div style="width:200px">
        <input id="Author[AuthorName][1]" type="text" name="Author[AuthorName][1]"/>
        <div id="ac1"></div>
      </div>
      <p/><br/><p/>
      Author:<br/>
      <div style="width:200px">
        <input id="Author[AuthorName][2]" type="text" name="Author[AuthorName][2]"/>
        <div id="ac2"></div>
      </div>
      <p/><br/><p/>
      <input type="submit" name="submit" value="Save" />
    </form>    
    
    <?php
    // open database connection
    $mysqli = new mysqli("localhost", "user", "pass", "library");      
    if (mysqli_connect_errno()) {
        printf("Connect failed: %s
", mysqli_connect_error());
        exit();
    }
    // retrieve author list
    $query = "SELECT AuthorName FROM author ORDER by AuthorName";
    $authors = array();
    if ($result = $mysqli->query($query)) {
      while ($row = $result->fetch_row()) {
        $authors[] = $row[0];
      }
      $result->close();
    }
    // clean up
    $mysqli->close();
    ?>      
  
    <script type="text/javascript">                
      // set data 
      arrayAuthors = [ 
        "<?php echo implode('","', $authors); ?>"
      ];       
      
      YAHOO.example.autocomplete = function() {
        var oConfigs = {
            prehighlightClassName: "yui-ac-prehighlight",
            queryDelay: 0,
            minQueryLength: 0,
            animVert: .01,
        }
        
        // instantiate local data source
        // code based on example at 
        // http://developer.yahoo.com/yui/examples/autocomplete/ac_basic_array.html
        var oDS = new YAHOO.util.LocalDataSource(arrayAuthors);
    
        // instantiate YUI autocomplete widgets
        var oAC0 = new YAHOO.widget.AutoComplete("Author[AuthorName][0]", "ac0", oDS, oConfigs);          
        var oAC1 = new YAHOO.widget.AutoComplete("Author[AuthorName][1]", "ac1", oDS, oConfigs);          
        var oAC2 = new YAHOO.widget.AutoComplete("Author[AuthorName][2]", "ac2", oDS, oConfigs);
        
        return {
            oDS: oDS,
            oAC0: oAC0,
            oAC1: oAC1,
            oAC2: oAC2
        };
      }();
     </script>
    
  </body>
</html>

You’ll notice also that in this version, the autocomplete suggestions appear quicker, because the widget is operating off a local array rather than a remote request. However, this method is only advisable when the data set is relatively small, as otherwise the inline JavaScript array might grow too large and consume an uncomfortably high amount of client memory.

Typing Tutor

An alternative to the previous example is to use HTML_QuickForm’s ‘autocomplete’ element, which also uses a local data cache and is fairly straightforward to implement. Here’s an example:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head></head>
  <body>
    <h3>Add Title</h3>

    <?php
    // open database connection
    $mysqli = new mysqli("localhost", "user", "pass", "library");      
    if (mysqli_connect_errno()) {
        printf("Connect failed: %s
", mysqli_connect_error());
        exit();
    }
    // retrieve author list
    $query = "SELECT AuthorName FROM author ORDER by AuthorName";
    $authors = array();
    if ($result = $mysqli->query($query)) {
      while ($row = $result->fetch_row()) {
        $authors[] = $row[0];
      }
      $result->close();
    }
    // clean up
    $mysqli->close();
        
    // initialize HTML_QuickForm object
    require_once 'HTML/QuickForm.php';
    $form = new HTML_QuickForm();
    
    // add auto-complete input field
    $title = $form->addElement('text', 'Title[TitleName]', 'Title:');
    $ac0 = $form->addElement('autocomplete', 'Author[AuthorName][0]', 'Author:');
    $ac1 = $form->addElement('autocomplete', 'Author[AuthorName][1]', 'Author:');
    $ac2 = $form->addElement('autocomplete', 'Author[AuthorName][2]', 'Author:');
    
    // add auto-complete options
    $ac0->setOptions($authors);
    $ac1->setOptions($authors);
    $ac2->setOptions($authors);
    
    // add submit button
    $form->addElement('submit', null, 'Submit');
    
    // print submitted values
    // render and display the form
    if ($form->validate()) {
      $form->freeze();
    } else {
      $form->display();
    }
    ?>
  </body>
</html>

Here, an ‘autocomplete’ element is first added to the form with addElement(), and then its setOption() method is used to set the array of values that will be used for auto-completion. When the form is rendered, as the user types a name into the autocomplete field, the input entered will be matched against this array of values (on every key press) and matching values, if any, will be used to complete the user’s input. Here’s an illustration of what this looks like:

In this example, the array is generated by PHP from a MySQL result set, as in the earlier examples. The HTML_QuickForm package will use this array and automatically generate the JavaScript code needed to enable the autocompletion feature (look in the source code of the rendered page to see it).

Dijit-al Download

If you’re a Dojo fan, then you’ll be thrilled to hear that Dijit, the Dojo widget library, also comes with a ComboBox component that exposes autocomplete functionality. This ComboBox can be linked to an ItemFileReadStore, which provides suggestions using a JSON source (although other formats are also supported). Here’s the code:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <style type="text/css">
      @import "/dojo-release-1.2.3/dijit/themes/tundra/tundra.css";
      @import "/dojo-release-1.2.3/dojo/resources/dojo.css"
    </style>  
    <script type="text/javascript" src="/dojo-release-1.2.3/dojo/dojo.js" djConfig="parseOnLoad: true"></script>
    <script type="text/javascript">
     dojo.require("dojo.parser");
     dojo.require("dijit.form.ComboBox");
     dojo.require("dojo.data.ItemFileReadStore");
    </script>
  </head>
  <body>
    <!-- 
    code based on example at

http://dojotoolkit.org/book/dojo-book-0-9/part-2-dijit/form-validation-specialized-input/auto-completer

    -->
    <h3>Add Title</h3>
    <form action="" method="post">
      <div dojoType="dojo.data.ItemFileReadStore" jsId="aStore" url="http://localhost/get_authors_json.php" />
      Title:<br/>
      <div class="tundra" style="width:200px">    
        <input type="text" dojoType="dijit.form.TextBox" name="Title[TitleName]" />
      </div>
      <p/>
      Author:<br/>
      <div class="tundra" style="width:200px;">
        <input name="Author[AuthorName][0]" dojoType="dijit.form.ComboBox" store="aStore" searchAttr="name" />           
      </div>
      <p/>
      Author:<br/>
      <div class="tundra" style="width:200px">
        <input name="Author[AuthorName][1]" dojoType="dijit.form.ComboBox" store="aStore" searchAttr="name" />           
      </div>
      <p/>
      Author:<br/>
      <div class="tundra" style="width:200px">
        <input name="Author[AuthorName][2]" dojoType="dijit.form.ComboBox" store="aStore" searchAttr="name" />           
      </div>
      <p/>
      <input type="submit" name="submit" value="Save" />
    </form>    
    
  </body>
</html>

Notice, in the listing above, that each ComboBox element has a ‘store’ attribute, which points to the Dojo ItemFileReadStore. This ItemFileReadStore looks for JSON data to be in a particular format, which is documented at http://dojotoolkit.org/book/dojo-book-0-9/part-3-programmatic-dijit-and-dojo/what-dojo-data/available-stores/dojo-data-item. It’s quite easy to generate this JSON data using a server-side PHP script, which is exactly what the script at ‘http://localhost/get_authors_json.php’ does:

<?php
// open database connection
$mysqli = new mysqli("localhost", "user", "pass", "library");      
if (mysqli_connect_errno()) {
    printf("Connect failed: %s
", mysqli_connect_error());
    exit();
}
// retrieve author list matching input
// add to XML document
$sql = "SELECT AuthorName FROM author ORDER by AuthorName";
if ($result = $mysqli->query($sql)) {
  while ($row = $result->fetch_row()) {
    $authors[] = array('name' => $row[0]);
  }
  $result->close();
}

// clean up
// output JSON
$mysqli->close();
echo json_encode(array('identifier' => 'name', 'items' => $authors));
?>

Here’s an example of the JSON output:

{"identifier":"name",
 "items":[
          {"name":"A. J. Quinnell"},
          {"name":"Agatha Christie"},
          ...
          {"name":"William McKay"},
          {"name":"William Rotsler"}
         ]
}

And here’s what the final product looks like:

As these examples demonstrate, it’s fairly easy to build an autocomplete input that uses a database for suggestions, by mixing a little PHP with ready-made widgets from toolkit such as Dojo or YUI. This article listed four possible ways in which this could be done…but there are many more! Play with it a little, discover your own…and don’t forget to post a comment telling everyone about it!

Copyright Melonfire, 2009. All rights reserved.

About Vikram Vaswani

Vikram Vaswani is the founder and CEO of "Melonfire":http://www.melonfire.com/, a consultancy specializing in open-source tools and technologies. He is a passionate proponent of the open-source movement and frequently contributes articles and tutorials on open-source technologies, including Perl, Python, PHP, MySQL, and Linux, to the community at large. He is the author of four books on PHP and MySQL, including "MySQL: The Complete Reference":http://www.mysql-tcr.com/, "How to Do Everything with PHP and MySQL":http://www.everythingphpmysql.com/ and "PHP Programming Solutions":http://www.php-programming-solutions.com/. Vikram has more than eight years of experience working with PHP and MySQL as an application developer. He is the author of Zend Technologies' "PHP 101 series":http://devzone.zend.com/tag/PHP101 for PHP beginners, and has extensive experience deploying PHP in a variety of different environments (including corporate intranets, high-traffic Internet Web sites, and mission-critical thin client applications). A Felix Scholar at the University of Oxford, England, Vikram combines his interest in Web application development with various other activities. When not dreaming up plans for world domination, he amuses himself by reading crime fiction, watching old movies, playing squash, blogging, and keeping an eye out for unfriendly Agents.

View all posts by Vikram Vaswani

6 Responses to “Building AutoComplete Inputs with PHP, PEAR, Dojo and YUI”

  1. wmakend Says:

    Thank you for this contribution. This script help realize my lot

  2. wmakend Says:

    I try it in FF you can see but not in IE are you nkwo why???

  3. sumitjoshi2010 Says:

    You did great job..thanks for this….
    since long time i was searching it….
    again thanks….

  4. _____anonymous_____ Says:

    Just wondering why version 3.2.9 of HTML_Quickform was used for this article as version 3.2.10 had been released back in october 2007!

  5. ylczjh Says:

    wonderful job!

  6. rfzend Says:

    For YUI Autocomplete Remote XML Request:

    Quick question, does YUI specify ‘query’ as the name of the value sent to the xml page?

    Per this line:
    $_GET['query']

    If not, what is the best way to specify that ‘query’ will be sent to the XML document?

    Thank you