Generating and Validating Web Forms With PEAR HTML_QuickForm (part 2)

First Among Sequels

In the first part of this article, I introduced you to HTML_QuickForm, PEAR's contribution to doing forms right. I showed you the basics of using HTML_QuickForm to programmatically generate a Web form, and explained the basics of both built-in and custom validation rules. Finally, I showed you how to assign default values to your form fields, and how to enhance HTML_QuickForm's server-side form validation with an extra layer of (automatic) client-side validation.

In this second (and concluding) segment, I'll be giving you a brief glimpse of other aspects of HTML_QuickForm that I think you would find educational, entertaining or both. I'll be discussing some of the package's non-standard form elements, teaching you how to combine elements into groups, showing you how to apply templates to control a form's appearance, and guiding you through the process of writing and registering your own custom validation rules. That's a lot of ground to cover, so you should stop dawdling over this introduction and flip the page to get started.

Group Think

First up, "element groups". As the name suggests, these are logical groupings of elements that have some tenuous or not-so-tenuous connection to each other. Here's an illustrative example:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// initialize group array
$group = array();

// add checkbox elements to group
$group[] = $form->createElement('checkbox', 'phone', null, 'Telephone');
$group[] = $form->createElement('checkbox', 'fax', null, 'Fax');
$group[] = $form->createElement('checkbox', 'email', null, 'Email');

// add group to form
$form->addGroup($group, 'contact', 'Contact methods:');

// add submit button
$form->addElement('submit', null, 'Submit');

// print submitted values
// render and display the form
if ($form->isSubmitted()) {
  print_r($form->exportValues());  
} else {
  $form->display();
}
?>
</body>
</html>


This example uses the createElement() method instead of the addElement() method to generate three checkboxes; this method returns an object representation of the elements added, and these are then linked together by adding them to an array. The addGroup() method is then used to add these elements to the form as a group named 'contact'.

The benefit of using a group becomes clearer once you process the form submission: elements of a group are returned as keys of an associative array, making this feature particularly good for linked elements. Here's a sample of the output:

Array
(
    [contact] => Array
        (
            [fax] => 1
            [email] => 1
        )
)


Just as you can set validation rules on a per-element basis, so too can you attach rules to element groups. The method to use is named addGroupRule(), and it should be passed a nested array of values, with the keys of the outer array representing the fields to be validated and the corresponding values holding the validation rules. Consider the following example, which illustrates:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// initialize group array
$group = array();

// add input elements to group
$group[] = $form->createElement('text', 'addr1', 'Address line 1', array('size' => 50));
$group[] = $form->createElement('text', 'addr2', 'Address line 2', array('size' => 50));
$group[] = $form->createElement('text', 'addr3', 'Address line 3', array('size' => 50));
$group[] = $form->createElement('text', 'postcode', 'Postcode', array('size' => '12'));

// add group to form
$form->addGroup($group, 'address', 'Shipping address:', '<br/>', false);

// add group rule to form
$form->addGroupRule('address', array(
  'addr1' => array(
    array('ERROR: Address line 1 is required', 'required')),
  'postcode' => array(
    array('ERROR: Postcode is required', 'required'),
    array('ERROR: Postcode must be numeric', 'numeric'))
));

// add submit button
$form->addElement('submit', null, 'Submit');

// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo 'Thank you for your submission';
} else {
  $form->display();  
}
?>
</body>
</html>


This script creates a logical group of four address fields, adds them to the form using the addGroup() method discussed previously, and then attaches validation rules to them using the addGroupRule() method. As per the validation rules, the first address line and postcode are mandatory, and the postcode is further constrained to numbers only.

Making Up The Rules

You may remember, from the first part of this article, how HTML_QuickForm allows you to write custom callback functions for input validation. In case you don't, here's a quick reminder - a script using a callback function that internally invokes PHP's checkdate() function to verify if a date entered by the user is a "real" date:

<html>
<head></head>
<body>
<?php
// function to check date validity
function checkDateInput($value) {
  if (checkdate($value['M'], $value['d'], $value['Y'])) {
    return true;
  } else {
    return false;
  }      
}
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// add date
$date = $form->addElement('date', 'issue', 'Passport issue date:', array('format' => 'dMY', 'minYear' => 1950, 'maxYear' => date('Y'))); 
$date = $form->addElement('date', 'expiry', 'Passport expiry date:', array('format' => 'dMY', 'minYear' => 1950, 'maxYear' => date('Y')+25)); 

// add date validation
$form->addRule('issue', 'ERROR: Date is invalid', 'callback', 'checkDateInput');
$form->addRule('expiry', 'ERROR: Date is invalid', 'callback', 'checkDateInput');

// add submit button
$form->addElement('submit', null, 'Submit');

// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo 'Thank you for your submission';
} else {
  $form->display();  
}
?>
</body>
</html>

If this turns out to be a validation routine that will be used in multiple places within your application, you can formalize it as a rule, by subclassing the HTML_QuickForm_Rule object. Here's how:

<?php
require_once 'HTML/QuickForm/Rule.php';

class HTML_QuickForm_Rule_Date extends HTML_QuickForm_Rule
{
    function validate($value, $options=null)
    {
        return checkdate($value['M'], $value['d'], $value['Y']);
    } 

    function getValidationScript($options = null)
    {
    }
}
?>


Save this class as a separate file in the HTML_QuickForm package's Rule sub-directory, and include it in all your scripts. Then, register this new rule in your PHP script by invoking HTML_QuickForm's registerRule() method and use it in the normal fashion, via the addRule() method. Here's a revision of the previous example, which illustrates how this works:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
require_once 'HTML/QuickForm/Rule/Date.php';
$form = new HTML_QuickForm();

// register custom rule
$form->registerRule('date', null, 'HTML_QuickForm_Rule_Date');

// add date
$date = $form->addElement('date', 'issue', 'Passport issue date:', array('format' => 'dMY', 'minYear' => 1950, 'maxYear' => date('Y'))); 
$date = $form->addElement('date', 'expiry', 'Passport expiry date:', array('format' => 'dMY', 'minYear' => 1950, 'maxYear' => date('Y')+25)); 

// use custom rule
$form->addRule('issue', 'ERROR: Date is invalid', 'date');
$form->addRule('expiry', 'ERROR: Date is invalid', 'date');

// add submit button
$form->addElement('submit', null, 'Submit');

// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo 'Thank you for your submission';
} else {
  $form->display();  
}
?>
</body>
</html>


Subclassing a validation rule in this manner lets you formally add it into HTML_QuickForm's validation model; it also lets you centralize the validation code in a single class, for easier maintenance or troubleshooting.

A Question Of Hierarchy

In addition to all the standard form elements, HTML_QuickForm also supports a few additional elements, all of which are definitively non-standard. The first of these is the hierarchical selection list or 'hierselect' element, which allows you to create a series of linked selection lists, such that selection of an item in the first list automatically changes the selection options available in the next list, and so on. This element is particularly handy when dealing with two related categories of options: countries and their cities, manufacturers and their brands, categories and their contents, and so on.

To see this element in action, consider the following example, which illustrates:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// level 0 array
$letters = array();
$letters[0] = 'A';
$letters[1] = 'B';
$letters[2] = 'C';

// level 1 array
$words = array();
$words[0][0] = 'Apple';
$words[0][1] = 'Anteater';
$words[0][2] = 'Armadillo';
$words[1][0] = 'Ball';
$words[1][1] = 'Beach';
$words[2][0] = 'Cat';
$words[2][1] = 'Chicken';
$words[2][2] = 'Can';
$words[2][3] = 'Computer';

// add hierselect element
$select = $form->addElement('hierselect', 'words', 'Words:');
$select->setOptions(array($letters, $words));

// add submit button
$form->addElement('submit', null, 'Submit');

// print submitted values
// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo 'You submitted the following data:';
  print_r($form->exportValues());  
} else {
  $form->display();
}
?>
</body>
</html>


A hierarchical selection list, once created with addElement(), is populated with a call to the element's setOptions() method. This method must be passed an array which contains further arrays, each representing one "level" of the hierarchical option list. Thus, in the previous example, $letters is an array containing the list of options for the first selection list, and $words is an array containing the list of options for the second (dependent) list. Notice how these arrays are linked together to reflect the dependence of list options: the first-level keys of $words correspond to the keys of $letters. Thus, for example, 'Beach' is the second option available under 'B', while 'Computer' is the fourth option available under 'C'.

Here's an image of what the output looks like:

Selecting an item from the first list will automatically reset the second list with the list of dependent items.

In most cases, however, the various permutations for a hierarchical selection element will not be explicitly coded, but will come from an external data source such as a database or XML file. Consider, therefore, a more real-world illustration of the previous example, which begins with the following two database tables:

mysql> SELECT * FROM Countries;
+-----------+----------------+
| CountryID | CountryName    |
+-----------+----------------+
|         1 | Australia      |
|         2 | Austria        |
|         3 | India          |
|         4 | United Kingdom |
|         5 | United States  |
+-----------+----------------+
5 rows in set (0.13 sec)

mysql> SELECT * FROM Cities;
+--------+-----------+-----------+
| CityID | CityName  | CountryID |
+--------+-----------+-----------+
|      1 | Sydney    |         1 |
|      2 | Melbourne |         1 |
|      3 | Vienna    |         2 |
|      4 | Salzburg  |         2 |
|      5 | Mumbai    |         3 |
|      6 | Delhi     |         3 |
|      7 | Calcutta  |         3 |
|      8 | London    |         4 |
|      9 | Oxford    |         4 |
|     10 | Cambridge |         4 |
|     11 | New York  |         5 |
|     12 | Boston    |         5 |
+--------+-----------+-----------+
12 rows in set (0.65 sec)


It should be clear how these two tables are related: cities are linked to countries by means of the CountryID field present in both tables. This kind of arrangement is ideal for dynamically building a hierarchical selection list - as you'll see in the next listing:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// create level 0 and level 1 arrays
// from database queries
try {
    $dbh = new PDO('mysql:host=localhost;dbname=test', 'john', 'doe');
    foreach ($dbh->query('SELECT CountryID, CountryName from Countries') as $row) {
        $id = $row['CountryID'];
        $name = $row['CountryName'];
        $countries[$id] = $name;
        foreach ($dbh->query("SELECT CityID, CityName from Cities WHERE CountryID = '$id'") as $row2) {
            $cid = $row2['CityID'];
            $cname = $row2['CityName'];
            $cities[$id][$cid] = $cname;
        }        
    }
    unset($dbh);
} catch (PDOException $e) {
    die ($e->getMessage());
}

// add hierselect element
$select = $form->addElement('hierselect', 'country', 'Country/City:');
$select->setOptions(array($countries, $cities));

// add submit button
$form->addElement('submit', null, 'Submit');

// print submitted values
// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo 'You submitted the following data:';
  print_r($form->exportValues());  
} else {
  $form->display();
}
?>
</body>
</html>


Here, the arrays used to populate the hierarchical selection list are dynamically generated through a series of nested SQL queries to the tables shown previously. These arrays are then passed to the element's setOptions() method, generating two linked selection lists. Here's what the output looks like:

Selecting a country from the first list will automatically reset the second list with a list of cities located in that country.

Typing Tutor

Another interesting element is HTML_QuickForm's 'autocomplete' element, which consists of a text input field which can automatically complete your input for you as you type. It's simple to use, and I thought it was pretty cool. Consider the following example, which illustrates:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// add auto-complete input field
$ac = $form->addElement('autocomplete', 'tz', 'Timezone:');

// add auto-complete options
$ac->setOptions(DateTimeZone::listIdentifiers());

// add submit button
$form->addElement('submit', null, 'Submit');

// print submitted values
// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo 'You submitted the following data: ';
  echo $form->exportValue('tz');  
} 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. In this example, the array comes from PHP's DateTimeZone object, whose listIdentifiers() method returns an array of time zone names, as below:

Array
(
    [0] => Africa/Abidjan
    [1] => Africa/Accra
    [2] => Africa/Addis_Ababa
    [3] => Africa/Algiers
    [4] => Africa/Asmara
    [5] => Africa/Asmera
    [6] => Africa/Bamako
    ....
)


When the form is rendered, as the user types a time zone 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:

HTML_QuickForm also offers a souped-up checkbox, one which allows you to submit a value to the processing script even when the checkbox isn't checked (a normal checkbox, if unchecked, does not submit any value to the processing script). This 'advcheckbox' element is illustrated in the next example:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// add advcheckbox element
$form->addElement('advcheckbox', 'obey', 'Do you swear to obey me, the great Lord Taxdojs?', null, null, array('Not while a breath remains in my body, you dictatorial warthog!', 'Of course, your Lordship!'));

// add submit button
$form->addElement('submit', null, 'Submit');

// print submitted values
// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo $form->exportValue('obey'); 
} else {
  $form->display();
}
?>
</body>
</html>


Notice the array passed to addElement() as the sixth parameter in the listing above: it holds two values, one for when the form is submitted with the checkbox unselected, and one for when the checkbox is selected. When you try submitting this form, you'll see that the submission contains a value for the 'advcheckbox' element, even if it remains unchecked at the time of submission. This is useful in certain situations: for example, if you have a list of files, each with a checkbox next to it, and you need to generate both an "include list" and an "exclude list" based on which files are selected.

Better, Faster, Stronger

One final element that bears mentioning, even though it's not really part of the HTML_QuickForm package: the 'advmultiselect' element. As you might have gathered from the name, this element is used for an advanced multiple-selection list: it generates two multiple-selection list boxes, one containing the available options and the other empty, and allows users to transfer items from one to the other either by double-clicking them or by using clickable buttons. The two list boxes, the clickable buttons and the JavaScript event handling routines are all automatically generated by the element.

Sounds impressive? It is! To see it in action, download and install the HTML_QuickForm_advmultiselect add-on package from http://pear.php.net/HTML_QuickForm_advmultiselect, and then try running the script below:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
require_once 'HTML/QuickForm/advmultiselect.php';
$form = new HTML_QuickForm();

// array of values
$playlist = array('Track 1', 'Track 2', 'Track 3', 'Track 4', 'Track 5');

// add multi-select boxes
$form->addElement('advmultiselect', 'playlist', array('Create your playlist', 'Tracks:', 'Playlist:'), $playlist);

// add submit button
$form->addElement('submit', null, 'Submit');

// print submitted values
// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo 'You submitted the following data: ';
  echo $form->exportValue('playlist');  
} else {
  $form->display();
  echo '<script src="qfamsHandler.js"></script>';
}
?>
</body>
</html>


Here's what it looks like:

Play with it a bit and you'll see that can transfer items from the source list (left) to the destination list (right) by double-clicking them, or by using the >> transfer button. Similarly, you can remove items from the destination list by double-clicking them, or by using the << transfer button.

As you can imagine, this type of ready-to-use element is quite handy when building user-friendly form interfaces. It's also worth noting that the package documentation includes numerous examples of using templates and styles to customize both the layout and appearance of the selection boxes and the clickable buttons.

One important note: you might need to copy the JavaScript code file 'qfamsHandler.js' to your work directory and explicitly include it in your output HTML page, as in the script above, in order to get the multi-select element to work correctly.

The Default Option

As previous examples have demonstrated, HTML_QuickForm automatically generates the HTML code needed to render a form and its elements. If you're the kind that likes tweaking things, though, or if you need to customize the appearance of your forms so that they better fit in to your application's design, you're bound to be unhappy with this state of affairs. That's why I'm going to spend the next couple of pages showing you how to customize the appearance of HTML_QuickForm's form and form elements.

First up, a word on renderers. As the name suggests, these are the bits and bobs that are responsible for actually outputting, or rendering, the form. HTML_QuickForm comes with 8 built-in renderers, some of which allow for direct modification of the form template and others which support more extensive form customization through external template engines such as Smarty.

The first, and simplest, renderer is the default renderer, aptly represented by an object named HTML_QuickForm_Renderer_Default. This is the default renderer for form output, and it's fairly easy to customize if you're not looking at performing major surgery on the form output. Let's look at an example of it in action:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm and HTML_QuickForm_Renderer objects
require_once 'HTML/QuickForm.php';
require_once 'HTML/QuickForm/Renderer/Default.php';
$form = new HTML_QuickForm();
$renderer =& new HTML_QuickForm_Renderer_Default();

// add input boxes
$form->addElement('text', 'fname', 'First name:', array('size' => 30));
$form->addElement('text', 'lname', 'Last name:', array('size' => 30));

// add password element
$form->addElement('password', 'pass', 'Password:', array('size' => 30));

// add radio buttons
$form->addElement('radio', 'sex', 'Sex:', 'Male', 'male');
$form->addElement('radio', 'sex', '', 'Female', 'female');

// add selection list
$country = $form->addElement('select', 'country', 'Country:', array(
  'us' => 'United States',
  'uk' => 'United Kingdom',
  'au' => 'Australia',
  'zz' => 'Other'
));

// add submit button
$form->addElement('submit', null, 'Submit');

// add validation rules
$form->addRule('fname', 'ERROR: Missing value', 'required');
$form->addRule('pass', 'ERROR: Missing value', 'required');

// set new element template
$elementTpl = <<< ELEMENT
  <div class="outer">
    <div class="error"><!-- BEGIN error --><span style="color: #ff0000">{error}</span><!-- END error --></div>
    <div class="label"><!-- BEGIN required --><span style="color: #ff0000">*</span><!-- END required --><b>{label}</b></div>
    <div class="element">{element}</div>
  </div>

ELEMENT;

// set new form template
$formTpl = <<< FORM
<form{attributes}>
{hidden}
{content}
</form>
FORM;

// attach templates to renderer
$renderer->setElementTemplate($elementTpl);
$renderer->setFormTemplate($formTpl);

// validate input
if ($form->validate()) {
  $form->freeze();
  echo 'You submitted the following data: ';
} 

// link form to renderer
$form->accept($renderer);

// render and display the form
echo $renderer->toHtml();
?>
</body>
</html>


There are two important methods to keep in mind when using HTML_QuickForm_Renderer_Default: the setElementTemplate() method and the setFormTemplate() methods. Both these methods accept HTML templates, which are then used to render, respectively, form elements and the outer <form> wrapper. These methods, therefore, serve as the primary point for injection of customized HTML layouts into HTML_Quickform.

The script above illustrates the process. First, the HTML_QuickForm_Renderer_Default class file is loaded into the script, and an instance of the class is created. Next, standard HTML_QuickForm methods are used to add elements and validation rules to the form. Following this, two HTML templates are defined: $elementTpl holds the HTML code that will be used when outputting elements of the form, while $formTpl holds the HTML code for the outermost <form> element. From a HTML perspective, these templates aren't particularly fancy (although they can be): in this instance, all they're doing is replacing the default <table> constructs used by HTML_QuickForm with <div>s.

Notice that these templates, in addition to holding standard HTML markup, also contain various symbols enclosed in curly braces: these symbols will be automatically replaced by actual data once the form is rendered. If you've ever used a template engine like Smarty, this approach should be very familiar to you. Thus, for example, the symbol {label} in the element template will be replaced by the human-readable label of the form field, while the {element} symbol will be replaced by the HTML code necessary to generate the corresponding field. Similarly, within the form template, the symbol {hidden} symbol will be automatically replaced with the form's hidden elements, and the {content} symbol will be replaced by the form's contents.

Once defined, the form and element templates are attached to the renderer via the previously-described setFormTemplate() and setElementTemplate() methods. The renderer is then itself linked to the main HTML_QuickForm object via the latter's accept() method, and the form is then output to the Web page via a call to the renderer's toHtml() method. Here's what it looks like:

Look in the source of the output page, and you'll see that the form's HTML code has been generated from the two templates specified earlier in the script.

Incidentally, the HTML_QuickForm_Renderer_Default class also comes with a few other methods to customize the form output: the setHeaderTemplate() lets you define a new template for the form header, the setGroupTemplate() lets you define a template for element groups, and the setRequiredNoteTemplate() allows you to customize the message for mandatory fields. You can read more about these methods in the HTML_QuickForm documentation.

Getting Smart-y

The HTML_QuickForm_Renderer_Default is pretty simple to understand and use, especially when all you need are minor cosmetic changes to your forms. When you're looking for more extensive customization, however, this default renderer may well prove to be insufficient. For these situations, HTML_QuickForm provides renderers that can be integrated with template engines, to offer even greater control over the output. Various template engines are supported, including Smarty, Flexy and IT, and you can also "roll your own" renderer to support a custom template engine.

This next example will demonstrate how HTML_Quickform can be integrated with the Smarty template engine using yet another built-in renderer, HTML_QuickForm_Renderer_Array. Before proceeding, check that you have Smarty installed and configured for use; in case you don't, you can download it from http://smarty.php.net/ and follow the (easy) installation instructions to get it up and running.

Now, I'll assume here that the task is to generate a customized HTML page containing a Web form, as well as various other elements. This page will be generated from a Smarty page template, and integrated by HTML_QuickForm using the HTML_QuickForm_Renderer_Array renderer. Here's the code:

<?php
// initialize form, renderer and Smarty objects
require_once 'HTML/QuickForm.php';
require_once 'HTML/QuickForm/Renderer/Array.php';
require_once 'Smarty.class.php';
$form = new HTML_QuickForm();
$smarty = new Smarty;
$renderer =& new HTML_QuickForm_Renderer_Array(false, true);

// set Smarty directories
$smarty->template_dir = './templates';
$smarty->compile_dir  = './templates_c';

// add input boxes
$form->addElement('text', 'fname', 'First name:', array('size' => 30));
$form->addElement('text', 'lname', 'Last name:', array('size' => 30));

// add password element
$form->addElement('password', 'pass', 'Password:', array('size' => 30));

// add radio buttons
$form->addElement('radio', 'sex', 'Sex:', 'Male', 'male');
$form->addElement('radio', 'sex', '', 'Female', 'female');

// add selection list
$country = $form->addElement('select', 'country', 'Country:', array(
  'us' => 'United States',
  'uk' => 'United Kingdom',
  'au' => 'Australia',
  'zz' => 'Other'
));

// add validation rules
$form->addRule('fname', 'ERROR: Missing value', 'required');
$form->addRule('pass', 'ERROR: Missing value', 'required');

// add submit button
$form->addElement('submit', null, 'Submit');

// if input valid
// freeze the form
if ($form->validate()) {
  $form->freeze();
  echo 'You submitted the following data: ';
} 

// link form to renderer
$form->accept($renderer);

// get form as array
$data = $renderer->toArray();

// assign template variables from form array
foreach ($data as $k => $v) {
    $smarty->assign($k, $v);
}

// render and display form
$smarty->display('form.tpl');
?>


Much of this is the same as before, except that a Smarty object is also created at the start of the script, in addition to form and renderer objects. Once elements and rules are added to the form, the renderer's toArray() method is used to retrieve the form as an associative array, and the keys of this array are then converted into template variables using Smarty's assign() method.

The array returned by the toArray() method looks something like this:

Array
(
    [frozen] => 
    [javascript] => 
    [attributes] =>  action="/dev/qform17.php" method="post" name="" id=""
    [requirednote] => <span style="font-size:80%; color:#ff0000;">*</span><span style="font-size:80%;"> denotes required field</span>
    [errors] => Array
        (
        )

    [elements] => Array
        (
            [0] => Array
                (
                    [name] => fname
                    [value] => 
                    [type] => text
                    [frozen] => 
                    [required] => 1
                    [error] => 
                    [label] => First name:
                    [html] => <input size="30" name="fname" type="text" />
                )

            [1] => Array
                (
                    [name] => lname
                    [value] => 
                    [type] => text
                    [frozen] => 
                    [required] => 
                    [error] => 
                    [label] => Last name:
                    [html] => <input size="30" name="lname" type="text" />
                )
      )
		...
)


Notice the 'elements' key of this array, which refers to a nested array. Each item in this nested array is itself an associative array, representing a form element. The array's keys thus represent various attributes of the element: the 'name' key contains the element name, the 'label' key the element's human-readable label, and the 'error' key any validation error(s) caused by the element.

Keeping this in mind, now look at the Smarty page template used by the script:

<html>
<head>
<script language="JavaScript">
{$javascript}
</script>
</head>
<body>

  <div id="header" style="height:30px; text-align:center; background-color:#EF6090">
    <h2>A template-driven form</h2>
  </div>
  <div id="container" style="padding: 10px; border:solid 5px; margin-left:10%; margin-right:10%; margin-top: 10%; background-color:#BFA4D0;">
    <form {$attributes}>
      {foreach from=$elements item=e}
      <div>
        <div style="font-weight:bold">{if $e.required}*{/if}{$e.label}</div>
        <div><span style="border:groove 1px;">{$e.html}</span></div>
        {if $e.error}        
        <div><span style="color:#092D82;font-weight:bold">{$e.error}</span></div>
        {/if}
      </div>    
      {/foreach}
    </form>
  </div>

</body>
</html>


Now, you already know that in the previous step, the keys of the array returned by the toArray() method were converted into template variables by Smarty's assign() method. One of these template variables is $elements (from the 'elements' key of the array) and it contains information on the various form elements. If you look in the page template, you'll see a foreach() loop for this array. When Smarty's display() method is called, this foreach() loop iterates over the $elements array and generates <div> blocks for each element, its label, and any error messages.

Here's what the final Web page looks like:

Tool Tips

The HTML_QuickForm package also comes with a couple of useful utility methods that are worth exploring. For example, there's the getRegisteredTypes() method, which returns a list of all supported element types, and the getRegisteredRules() method, which returns a list of all supported validation rules. Here's an example which demonstrates both these methods:

<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// get supported form elements
print_r($form->getRegisteredTypes());

// get supported validation rules
print_r($form->getRegisteredRules());
?>


Fields marked as 'required' are automatically marked with an asterisk and a message by HTML_QuickForm. If you'd prefer to change the default text of this message, head for the setRequiredNote() method, which allows you to define a different message. Here's an example:

<html>
<head></head>
<body>
<?php
// initialize HTML_QuickForm object
require_once 'HTML/QuickForm.php';
$form = new HTML_QuickForm();

// add input boxes
$form->addElement('password', 'pass', 'Password:', array('size' => 30));
$form->addElement('password', 'confirmpass', 'Confirm password:', array('size' => 30));

// add submit button
$form->addElement('submit', null, 'Submit');

// add validation rules
$form->addRule('pass', 'ERROR: Password missing', 'required');
$form->addRule('pass', 'ERROR: Password too short', 'minlength', 6);
$form->addRule('confirmpass', 'ERROR: Password missing', 'required');
$form->addRule(array('pass','confirmpass'), 'ERROR: Password mismatch', 'compare');

// change 'required' message
$form->setRequiredNote('Fields marked with <span style="color:red">*</span> are mandatory');
 
// print submitted values
// render and display the form
if ($form->validate()) {
  $form->freeze();
  echo 'You submitted the following data:';
} 

$form->display();
?>
</body>
</html>


Here's what the output looks like:

And that's about all we have time for. Over the course of this article, I showed you how to create and validate element groups; apply templates to control a form's appearance; register validation rules; and use non-standard form elements like hierarchical selection lists and auto-complete text input fields. Hopefully, this two-part series gave you some insight into the wonder that is HTML_QuickForm and motivated you to try it in your next Web development project. Good luck, and happy coding!

Copyright Melonfire 2007. All rights reserved.