I have found a great article on creating form validation http://simonwillison.net/2003/Jun/17/theHolyGrail/ but it is several years old and the author doesn't support it anymore.

This is exactly what I want and have got stuck ttrying to add a textarea validation.

I have added a textarea and I have got it validate but what i want it to be able to do is set the colour the same as the other fields with errors.

Does anybody know how i can do this?

This is the page with the form
PHP Code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
  <title>FormProcessor Demo</title>
  <style type="text/css">
body {
  font-family: georgia;
  margin: 2em;    
}
div label {
  display: block;
  font-size: 0.8em;
}
div {
  margin-bottom: 0.5em;    
}
input.invalid {
  background-color: pink;
}
strong.error {
  color: red;
}
  </style>
</head>
<body>
<h1>FormProcessor Demo</h1>
<?php

include('FormProcessor.php');

$form = <<<EOD
<form action="test.php" method="post">
<errorlist>
<ul>
<erroritem> <li><message /></li>
</erroritem></ul>
</errorlist>
<div>
 <label for="name">Name: </label>
 <input type="text" name="name" id="name" compulsory="yes" validate="alpha" callback="uniqueName" size="20" /><error for="name"> <strong class="error">!</strong></error>
</div>
<errormsg field="name" test="compulsory">You did not enter your name</errormsg>
<errormsg field="name" test="alpha">Your name must consist <em>only</em> of letters</errormsg>
<div>
 <label for="email">Email: </label>
 <input type="text" name="email" id="email" compulsory="yes" validate="email" size="20" /><error for="email"> <strong class="error">!</strong></error>
</div>
<errormsg field="email" test="compulsory">You must provide an email address</errormsg>
<errormsg field="email" test="validate">Your email address is invalid</errormsg>
<div>
 <label for="pass1">Password: </label>
 <input type="password" name="pass1" id="pass1" compulsory="yes" validate="alphanumeric" size="10" /><error for="pass1"> <strong class="error">!</strong></error>
</div>
<errormsg field="pass1" test="compulsory">You did not provide a password</errormsg>
<errormsg field="pass1" test="validate">Your password must contain only letters and numbers</errormsg>
<div>
 <label for="pass2">Repeat password: </label>
 <input type="password" name="pass2" id="pass2" validate="alphanumeric" mustmatch="pass1" size="10" /><error for="pass2"> <strong class="error">!</strong></error>
</div>
<errormsg field="pass2" test="mustmatch">The two passwords did not match</errormsg>
<div>
 <label for="message">Message: </label>
 <textarea name="message" id="message" compulsory="yes" validate="alphanumeric" rows="20" cols="45"></textarea><error for="message"> <strong class="error">!</strong></error>
</div>
<errormsg field="message" test="compulsory">You forgot to write your message</errormsg>
<div><input type="submit" value="Create Account" /></div>
</form>
EOD;

function 
uniqueName($name) {
    return 
true;
}

$processor =& new FormProcessor($form);
if (
$processor->validate()) {
    echo 
'<p>Form data is OK.</p><pre>';
    
print_r($_POST);
    echo 
'</pre>';
} else {
    
$processor->display();
}

?>
</body>
</html>
This is the validation functions
PHP Code:
<?php

/* FormProcessor 0.3
   Simon Willison, 17th June 2003
   A work in progress
*/

class FormProcessor {
    var 
$formML;
    var 
$rules = array();
    var 
$errormsgs = array();
    var 
$errors = array();
    var 
$output '';
    
    var 
$errorClass 'invalid';
    
// Properties used during XML parsing for the form() method
    
var $_parser;
    var 
$_copy true// Flag: should formML contents be copied to output?
    
var $_saveForTemplate false// Flag: are we saving this for an erroritem template?
    
var $_errorTemplate '';
    var 
$_inTextArea false;
    function 
FormProcessor($formML) {
        
$this->formML $formML;
        
// Extract the validation rules
        
$extractor =& new FormRuleExtractor($formML);
        
$this->rules =& $extractor->rules;
        
$this->errormsgs =& $extractor->errormsgs;
    }
    function 
validate() {
        if (!
$_POST) {
            return 
false;
        }
        
$this->errors = array();
        foreach (
$this->rules as $name => $rules) {
            
// Check if compulsory field has not been filled
            
if ($rules['compulsory'] && (!isset($_POST[$name]) || trim($_POST[$name]) == '')) {
                
$this->errors[$name] = $this->getErrorMsg($name'compulsory');
                continue;
            }
            if (!isset(
$_POST[$name])) {
                
// Field not set, and it's not compulsory
                
continue;
            }
            
$value $_POST[$name];
            
// If a regular expression is specified, check using that
            
if (isset($rules['regexp']) && !preg_match($rules['regexp'], $value)) {
                
$this->errors[$name] = $this->getErrorMsg($name'regexp');
                continue;
            }
            
// If there is a mustmatch rule, check using that
            
if (isset($rules['mustmatch']) && $value != $_POST[$rules['mustmatch']]) {
                
$this->errors[$name] = $this->getErrorMsg($name'mustmatch');
                continue;
            }
            
// If there is a calback rule, run that function
            
if (isset($rules['callback'])) {
                
$callback $rules['callback'];
                if (
substr($callback05) == 'this:') {
                    
// It's actually a method on this class
                    
$method substr($callback5);
                    if (!
$this->$method($value)) {
                        
$this->errors[$name] = $this->getErrorMsg($name'callback');
                        continue;
                    }
                } else {
                    
// It's just a normal function
                    
if (!$callback($value)) {
                        
$this->errors[$name] = $this->getErrorMsg($name'callback');
                        continue;
                    }
                }
            }
        }
        
// All rules should now have been processed
        
return count($this->errors) == 0;
    }
    function 
display() {
        echo 
$this->form();
    }
    function 
form() {
        
// Returns the XHTML for the form, after processing
        
$this->output '';
        
$this->_parser xml_parser_create();
        
// Set XML parser to take the case of tags in to account
        
xml_parser_set_option($this->_parserXML_OPTION_CASE_FOLDINGfalse);
        
// Set XML parser callback functions
        
xml_set_object($this->_parser$this);
        
xml_set_element_handler($this->_parser'tag_open''tag_close');
        
xml_set_character_data_handler($this->_parser'cdata');
        if (!
xml_parse($this->_parser$this->formML)) {
            die(
sprintf('XML error: %s at line %d',
                
xml_error_string(xml_get_error_code($this->_parser)),
                
xml_get_current_line_number($this->_parser)));
        }
        
xml_parser_free($this->_parser);
        return 
$this->output;
    }
    function 
tag_open($parser$tag$attr) {
        if (!
$this->_copy) {
            return;
        }
        if (
$this->_saveForTemplate) {
            if (
$tag == 'message') {
                
$this->_errorTemplate .= '%%%MESSAGE_HERE%%%';
            } else {
                
$this->_errorTemplate .= '<'.$tag.$this->_makeAttr($attr);
                if (
in_array($tag, array('br''img'))) {
                    
$this->_errorTemplate .= ' />';
                } else {
                    
$this->_errorTemplate .= '>';
                }
            }
            return;
        }
        
$killattrs = array('compulsory''validate''regexp''callback''mustmatch''errormsg');
        
// Kill unrequired attributes
        
foreach ($killattrs as $a) {
            unset(
$attr[$a]);
        }
        switch (
$tag) {
            case 
'error':
                if (!
$_POST || !isset($this->errors[$attr['for']])) {
                    
$this->_copy false;
                }
                return;
            case 
'errormsg':
                
$this->_copy false;
                return;
            case 
'errorlist':
                if (!
$_POST) {
                    
// Stop copying
                    
$this->_copy false;
                    return;
                } else {
                    
// We are going to be redisplaying, so keep copying
                    
return;
                }
            case 
'erroritem':
                
$this->_saveForTemplate true;
                return;
            case 
'img':
            case 
'br':
                
// Empty tags
                
$this->output .= '<'.$tag.$this->_makeAttr($attr).' />';
                return;
            case 
'input':
                
// Add the value attribute, if redisplaying
                
if (isset($_POST[$attr['name']]) && $attr['type'] != 'password') {
                    
$attr['value'] = htmlentities(stripslashes($_POST[$attr['name']]));
                }
                
// Add en error class if an error occured
                
if (isset($this->errors[$attr['name']])) {
                    
$attr['class'] = isset($attr['class']) ? $attr['class'].' '.$this->errorClass $this->errorClass;
                }
                
$this->output .= '<'.$tag.$this->_makeAttr($attr).' />';
                return;
            case 
'textarea':
                
$this->_inTextArea $attr['name'];
        }
        
// Add tag to the output
        
$this->output .= '<'.$tag.$this->_makeAttr($attr).'>';
    }
    function 
cdata($parser$data) {
        if (
$this->_saveForTemplate) {
            
$this->_errorTemplate .= $data;
            return;
        }
        if (
$this->_copy) {
            
$this->output.= $data;
        }
    }
    function 
tag_close($parser$tag) {
        if (
$this->_saveForTemplate && $tag != 'erroritem' && !in_array($tag, array('br''img''message'))) {
            
$this->_errorTemplate .= '</'.$tag.'>';
            return;
        }
        switch (
$tag) {
            case 
'error':
            case 
'errormsg':
            case 
'errorlist':
                
$this->_copy true;
                break;
            case 
'erroritem':
                
// Stop saving this in the template
                
$this->_saveForTemplate false;
                
// Now output all error messages using that template
                
foreach ($this->errors as $name => $error) {
                    
$this->output .= str_replace('%%%MESSAGE_HERE%%%'$error$this->_errorTemplate);
                }
                return;
            case 
'textarea':
                if (isset(
$_POST[$this->_inTextArea])) {
                    
$this->output .= htmlentities(stripslashes($_POST[$this->_inTextArea]));
                }
                
$this->output .= '</textarea>';
                
$this->_inTextArea false;
                break;
            case 
'img':
            case 
'br':
            case 
'input':
            case 
'message':
                
// Empty tags
                
break;
            default:
                if (
$this->_copy) {
                    
$this->output.= '</'.$tag.'>';
                }
        }
    }
    function 
_makeAttr($attr) {
        
$html ' ';
        foreach (
$attr as $name => $value) {
            
$html .= $name.'="'.$value.'" ';
        }
        return 
substr($html0, -1); // Remove trailing space
    
}
    function 
getErrorMsg($field$test) {
        if (isset(
$this->errormsgs["$field:$test"])) {
            return 
$this->errormsgs["$field:$test"];
        }
        
// No error message has been specified - generate one based on the $test
        
switch ($test) {
            case 
'regexp':
                return 
"Field '$field' contained invalid data";
            case 
'compulsory':
                return 
"Field '$field' must be filled in ";
            case 
'mustmatch':
                return 
"'$field' must match '{$this->rules[$field]['mustmatch']}'";
            case 
'callback':
            default:
                return 
"Field '$field' was not valid";
        }
    }
    function 
checkEmail($email) {
        
// Used as callback or validate="email" shortcut
        
return preg_match(
            
'#^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$#',
            
$email
        
);
        
// Regexp from http://www.regexplib.com/REDetails.aspx?regexp_id=26
    
}
}

class 
FormRuleExtractor {
    
// Extracts the validation rules from the formML.
    // Implementation note: the 'validate' attribute is a convenience, it is converted in 
    // to either a regular expression rule or a callback
    
var $rules = array();
    var 
$errormsgs;
    
// Following variables used during XML parsing
    
var $_parser;
    var 
$_errorMsgName;
    var 
$_errorMsgValue;
    var 
$_collectErrorMsg false;
    function 
FormRuleExtractor($formML) {
        
$this->_parser xml_parser_create();
        
// Set XML parser to take the case of tags in to account
        
xml_parser_set_option($this->_parserXML_OPTION_CASE_FOLDINGfalse);
        
// Set XML parser callback functions
        
xml_set_object($this->_parser$this);
        
xml_set_element_handler($this->_parser'tag_open''tag_close');
        
xml_set_character_data_handler($this->_parser'cdata');
        if (!
xml_parse($this->_parser$formML)) {
            die(
sprintf('XML error: %s at line %d',
                
xml_error_string(xml_get_error_code($this->_parser)),
                
xml_get_current_line_number($this->_parser)));
        }
        
xml_parser_free($this->_parser);
    }
    function 
tag_open($parser$tag$attr) {
        
// First, the stuff to deal with the errormsg tag and contents
        
if ($tag == 'errormsg') {
            if (!isset(
$attr['field']) || !isset($attr['test'])) {
                
// Die noisily
                
die('FormML error: errormsg tag needs field and test attributes');
            }
            
$this->_errorMsgName $attr['field'].':'.$attr['test'];
            
$this->_errorMsgValue '';
            
$this->_collectErrorMsg true;
            return;
        }
        if (
$this->_collectErrorMsg) {
            
$this->_errorMsgValue .= '<'.$tag.$this->_makeAttr($attr);
            if (
in_array($tag, array('br''img'))) {
                
$this->_errorMsgValue .= ' />';
            } else {
                
$this->_errorMsgValue .= '>';
            }
        }
        
// Now the stuff to deal with everything else
        
if (!in_array($tag, array('input''select''textarea'))) {
            return;
        }
        
// Skip submit, image and reset fields
        
if (isset($attr['type']) && in_array($attr['type'], array('submit''reset''image'))) {
            return;
        }
        
$rules = array();
        if (isset(
$attr['type'])) {
            
$rules['type'] = $attr['type'];
        } else {
            
$rules['type'] = $tag;
        }
        
$name $attr['name'];
        
// compulsory="yes"
        
if (isset($attr['compulsory']) && $attr['compulsory'] == 'yes') {
            
$rules['compulsory'] = true;
        }
        
// validate="something"
        
if (isset($attr['validate'])) {
            switch (
$attr['validate']) {
                case 
'alpha':
                    
$rules['regexp'] = '|^[a-zA-Z\s]*$|';
                    break;
                case 
'alphanumeric':
                    
$rules['regexp'] = '|^[a-zA-Z0-9]*$|';
                    break;
                case 
'numeric':
                    
$rules['regexp'] = '|^[0-9]*$|';
                    break;
                case 
'email':
                    
$rules['callback'] = 'this:checkEmail';
                    break;
            }
        }
        
// callback="someFunction"
        
if (isset($attr['callback'])) {
            
$rules['callback'] = $attr['callback'];
        }
        
// regexp="someregexp"
        
if (isset($attr['regexp'])) {
            
$rules['regexp'] = $attr['regexp'];
        }
        
// mustmatch="something"
        
if (isset($attr['mustmatch'])) {
            
$rules['mustmatch'] = $attr['mustmatch'];
        }
        
// Save the rules to $this->rules
        
$this->rules[$name] = $rules;
    }
    function 
cdata($parser$data) {
        if (
$this->_collectErrorMsg) {
            
$this->_errorMsgValue .= $data;
        }
    }
    function 
tag_close($parser$tag) {
        if (
$tag == 'errormsg') {
            
$this->errormsgs[$this->_errorMsgName] = $this->_errorMsgValue;
            
$this->_collectErrorMsg false;
        }
        if (
$this->_collectErrorMsg && !in_array($tag, array('br''img'))) {
            
$this->_errorMsgValue .= '</'.$tag.'>';
        }
    }
    function 
_makeAttr($attr) {
        
$html ' ';
        foreach (
$attr as $name => $value) {
            
$html .= $name.'="'.$value.'" ';
        }
        return 
substr($html0, -1); // Remove trailing space
    
}
}

?>