Ajax Validation Component

By Garth Braithwaite (GarthDB)
This component adds a few validation options to the already wonderful CakePHP form validation. These features include:
Confirm - Checks two or more fields to make sure they are an exact match (designed for password creation).
Unique - Checks to make sure that a field is not already present in the database (ie email, username, etc).
More output options - There are three output options - unordered list in a div tag of your choice, a JavaScript alert, or just the array to do what you want with it.
Error Class - It has the option to change the CSS class of the label tags for the invalid fields.

The component uses Ajax to be able to validate without refreshing the page. It also relies heavily on the "Standard" CakePHP naming conventions for easier integration.
If you have any problems you can also email me at garth_dot_d_dot_braithwaite_at_gmail_dot_com. I have the component below, feel free to copy and put it in a new file in app/controllers/components/ajax_valid.php. Then you will need a come code in the view, and controller, so I will outline it below. I think there is a better way to do some of this so feel free to let me know any suggestions you might have.

Here is an example of a users controller.

Controller Class:

Download code <?php 
class UsersController extends AppController
{
    var 
$name 'User';
    var 
$components = array('AjaxValid');//Make sure you include this, it makes the magic work.
    
var $helpers = array('Html''Javascript''Ajax','Form'); //I use these in my views so I include them here.  You can do however you like.  If you don't know how to use helpers just put this line in your controller.
    
function add(){//This component is really designed for add and edit, but there are more uses than I have thought of.
        //here goes whatever you need for your add form and what not, the real work is done in the validator action.
    
}
    function 
validator(){//This is the validator action. ha!
        
$this->layout '';//It is best to give it a null layout because using ajax whatever is shown in this view will be the output on the form page
        
$this->AjaxValid->return 'javascript';//This specifies how you want the validation result returned.  Your three options here are javascript, html, and arrary.  The javascript returns as an alert (which bugs some people, but it is commonly used.  html is nicer, it returns as an unordered list that you can modify with css. array is just for debugging purposes.
        
$this->AjaxValid->changeClass('errors');//This is an option that will change the class of the erroneous field labels.  I use error, but use what you want.
        
$this->AjaxValid->setForm($this->data,'User/index','redirect');//This is where you send the data from the form through to the component, so you probably want to leave the first parameter as $this->data, the third tells it to redirect after it is valid, and it redirects to the url in the second parameter.
        
$this->AjaxValid->required(array('User/fname','User/lname','User/email','User/password'));//this means that you are requiring these fields to be valid according to the validation in the model.
        
$this->AjaxValid->unique(array('User/email');//This checks to make sure that the email entered by the user is unique.
        
$this->AjaxValid->confirm('User/password', array('User/confirm'), 'Your passwords do not match');//This is designed for creating a new password.  If you have a user type in a password and retype it to confirm they didn't screw it up.  It just checks the string in the first parameter matches all the strings in the array of the second parameter.  The third parameter is the text that is returned by the validator.
        
$this->set('data',$this->AjaxValid->validate());//This sends the validation result to the validator view.
        
$this->set('valid',$this->AjaxValid->valid);//This sends a boolean of the result of the validation to the validator view, whether it is true(valid) or false(invalid)
    
if($this->AjaxValid->valid){//In this example I have the validator action save the data from the form if it is valid and then the view will redirect it.
        
$this->User->save($this->AjaxValid->form['User'];
    }
}
?>
Here is the form view.. in this example the view for the add action

View Template:

Download code
<?php // you need to make sure you download the prototype and scriptaculous libraries are in your app/webroot/js folder.  If you don't know how to do this email me or ask around.  It is a pretty common task.
if(isset($javascript))
{
    echo 
$javascript->link('prototype');
    echo 
$javascript->link('scriptaculous.js');
}
?>
<style><? //This is just for the color change for erroneous fields, there might be a better place to put this style on your cake project ?>
.errors{
    color:#F00;
}
</style>
<? echo $ajax->form($params = array('action'=>'/users/validator'),$type = 'post',$options = array('url'=>'User/validator','update'=>'updater'));// This is why we have the ajax helper.  This is a ajax-form-open-tag-maker.  The important thing is the options array - make the url go to the validator and the update is the id of the div tag that ajax will dump the result of the validation into. ?>
<? echo $form->labelTag('User/email','Email');// This component utilizes the <label> tag to change the class of the erroneous fields. This is why we have the form helper ?>
<? echo $html->input('User/email'); //Then this is the actual input field?>
<? echo $form->labelTag('User/password','Password');?>
<? echo $html->password('User/password');?>
<? echo $form->labelTag('User/confirm','Confrim Password');?>
<? echo $html->password('User/confirm');//Please note that confirm does not get saved in the db, so it is not even a part of the user model?>
<? echo $form->labelTag('User/fname','First Name');?>
<? echo $html->input('User/fname');?>
<? echo $form->labelTag('User/fname','Last Name');?>
<? echo $html->input('User/lname');?>
<? echo $html->submit();?>
</form>
<div id="updater"></div><? //This is the div tag where the results of the validation go.?>
Here is the validator view

View Template:

Download code
<? if($valid){//This echos the result of the validation.
    echo "Valid";
} else {
    echo "not valid";
} ?>
<pre><? print_r($data);//I put this in a <pre> tag in case I need to debug and set the $this->AjaxValid->return to be "array" in the validator action so I can debug it easily. Take off the pre tag if you want the html result.?></pre>

Component Class:

Download code <?php 
class AjaxValidComponent extends Object{
    var 
$controller true;
    var 
$valid true;//Valid until proven otherwise
    
var $errors = array();//Where the list of errors will be stored
    
var $form = array();//Where the form data will be stored
    
var $return "array";
    var 
$html;
    var 
$javascript;
    var 
$classFlag false;
    var 
$actionUrl;
    var 
$method;
    function 
startup(&$controller){
        
// This method takes a reference to the controller which is loading it.
        // Perform controller initialization here.
        
$this->controller = &$controller;
    }
    function 
setForm($form = array(), $actionUrl null$method nulll){
        
$this->form $form;
        if (
$actionUrl!=null){
            if(
$method != null){
                
$this->method $method;
            }
            
$regEx "^(ftp|http|https)://(www.)?";
            if(!
ereg($regEx,$actionUrl)){
                
$regEx "^[A-Z][a-z]+/[a-z]+$";
                if(
ereg($regEx,$actionUrl)){
                    
$url_ar explode('/',$actionUrl);
                    
$actionUrl strrchr(ROOT"/").'/'.Inflector::pluralize(strtolower($url_ar[0])).'/'.$url_ar[1];
                }
            }
            
$this->actionUrl $actionUrl;
        } else {
            
$this->actionUrl false;
        }
    }
    function 
submit(){
        if(
$this->valid){
            
$elem Inflector::camelize(key($this->form)." ".key($this->form[key($this->form)]));
            
$submitStr "";
            
$submitStr "<script type='text/javascript'>";
            
$submitStr .= "elem = document.getElementById('".$elem."');\n";
            
$submitStr .= "elem.form.action = '".$this->actionUrl."';\n";
            
$submitStr .= "alert(elem.form.action);\n";
            
$submitStr .= "elem.form.method='POST';\n";
            
$submitStr .= "//elem.form.submit();\n";
            
$submitStr .= "</script>";
            return 
$submitStr;
        }
    }
    function 
jsRedirect(){
        if(
$this->valid){
            
$redirStr "";
            
$redirStr "<script type='text/javascript'>";
            
$redirStr .= "document.location = '".$this->actionUrl."';\n";
            
$redirStr .= "</script>";
            return 
$redirStr;
        }
    }
    function 
confirm($initField string$fields = array(), $errormsg string){
        
$init_ar explode("/",$initField);
        foreach(
$fields as $field){
            if(
$this->form[$init_ar[0]][$init_ar[1]] != $field){
                
$this->valid false;
                
$this->errors[$initField]['confirm'] = $errormsg;
                break;
            }
        }
    }
    function 
required($fields = array()){
        foreach (
$fields as $field){
            
$field_ar explode('/',$field);
            if(
is_array($this->controller->{$field_ar[0]}->validate[$field_ar[1]])){
                foreach (
$this->controller->{$field_ar[0]}->validate[$field_ar[1]] as $key => $required){
                    if(!
preg_match($required['expression'],$this->form[$field_ar[0]][$field_ar[1]])){
                        
$this->errors[$field]['required'][$key] = $required['message'];
                        
$this->valid false;
                    }
                }
            } else {
                if(!
preg_match($this->controller->{$field_ar[0]}->validate[$field_ar[1]],$this->form[$field_ar[0]][$field_ar[1]])){
                    
$this->errors[$field]['required'][$field_ar[1]] = Inflector::humanize(str_replace("_id","",$field_ar[1]))." is required.";
                    
$this->valid false;
                }
            }
        }
    }
    function 
unique($table = array()){
        foreach (
$table as $key => $fields):
            foreach(
$fields as $field):
                
$field_ar explode('/',$field);
                
$model $field_ar[0];
                
$fieldName $field_ar[1];
                
$tableField str_replace('/','.',$field);
                
$result $this->controller->User->find(array($tableField =>$this->form[$model][$fieldName]) ,$tableField);
                if(!empty(
$result)){
                    
$this->errors[$field]['unique'] = $this->form[$model][$fieldName].' already exsists in the db.';
                    
$this->valid false;
                }
            endforeach;
        endforeach;
    }
    function 
changeClass($errorClass string){
        
$this->classFlag $errorClass;
    }
    function 
changeClassFun (){
        if (!
$this->valid){
        
$classStr "";
        
$classStr "<script type='text/javascript'>";
        
$classStr .= "
function errorClass(id,newClass){
    var elem_ar = document.getElementsByTagName('label');
    var classOld = '';
    var labelFor = '';
    var elem;
    for(x in elem_ar){
        labelFor = elem_ar[x].htmlFor+'';
        if(labelFor.indexOf(id) != -1){
            elem = elem_ar[x];
        }
    }
    classOld = elem.className+'';
    if(classOld.indexOf(newClass) == -1){
        elem.className = newClass+' '+classOld;
    }
}
function validClass(id,newClass){
    var elem_ar = document.getElementsByTagName('label');
    var classOld = '';
    var labelFor = '';
    var elem = '';
    for(x in elem_ar){
        labelFor = elem_ar[x].htmlFor+'';
        if(labelFor.indexOf(id) != -1){
            elem = elem_ar[x];
        }
    }
    if(elem!=''){
        classOld = elem.className+'';
        if(classOld.indexOf(newClass+' ') != -1){
            elem.className = classOld.replace(newClass+' ','');
        } else if (classOld.indexOf(newClass) != -1){
            elem.className = classOld.replace(newClass,'');
        }
    }
}"
;
    foreach(
$this->form as $parentKey =>$parentVal):
        foreach (
$parentVal as $childKey => $childVal):
            
$childKey_cam Inflector::camelize($childKey);
            if(!empty(
$this->errors[$parentKey."/".$childKey])){
                
$classStr.="errorClass('".$parentKey.$childKey_cam."','".$this->classFlag."');
"
;
            } else {
                
$classStr.="validClass('".$parentKey.$childKey_cam."','".$this->classFlag."');
"
;
            }
        endforeach;
    endforeach;
    
$classStr .= "</script>" ;
    return 
$classStr;
    }
    }
    function 
validate (){
        switch (
$this->return){
        case 
'array':
            return 
$this->errors;
            break;
        case 
'html':
            
$this->html '<ul class="errorsList">';
            foreach (
$this->errors as $err_key => $err_val):
                
$this->html .='<li>'.ucfirst(substr($err_key,strpos($err_key,'/')+1));
                
$this->html .= '<ul class="errorChild">';
                foreach (
$err_val as $error1):
                        if(
is_array($error1)){
                        foreach (
$error1 as $error2):
                            
$this->html .='<li>'.$error2.'</li>';
                        endforeach;
                        } else {
                            
$this->html .='<li>'.$error1.'</li>';
                        }
                endforeach;
                
$this->html .='</ul></li>';
            endforeach;
            
$this->html .= '</ul>';
            if(
$this->classFlag != false){
                
$this->html .=$this->changeClassFun();
            }
            if(
$this->method == 'submit'){
                
$this->html .= $this->submit();
            }
            if(
$this->method == 'redirect'){
                
$this->html .= $this->jsRedirect();
            }
            return 
$this->html;
            break;
        case 
'javascript':
            if(!
$this->valid){
                
$this->javascript '<script type="text/javascript">alert("';
                
$this->javascript .= 'Please fix the following Errors:\\n';
                foreach (
$this->errors as $err_val):
                    foreach (
$err_val as $error1):
                        if(
is_array($error1)){
                            foreach (
$error1 as $error2):
                                
$this->javascript .='- '.$error2.'\\n';
                            endforeach;
                            } else {
                                
$this->javascript .='- '.$error1.'\\n';
                            }
                    endforeach;
                endforeach;
                
$this->javascript .='");</script>';
            }
            if(
$this->classFlag != false){
                
$this->javascript .=$this->changeClassFun();
            }
            if(
$this->method == 'submit'){
                
$this->javascript .= $this->submit();
            }
            if(
$this->method == 'redirect'){
                
$this->javascript .= $this->jsRedirect();
            }
            return 
$this->javascript;
            break;
        case 
'test':
            return 
$this->submit();
            break;
        }
    }
}
?>

 

Comments 88

CakePHP Team Comments Author Comments
 

Comment

1 An Example

Having put in all this work on the component, and putting it up on here, not showing an example seems discouraging.
Posted Oct 15, 2006 by Lindsey Simon
 

Comment

2 It was a draft

I apologize, it was a draft, but was mistakenly published. Please take a look now to see if works for you now.

Having put in all this work on the component, and putting it up on here, not showing an example seems discouraging.
Posted Oct 25, 2006 by Garth Braithwaite
 

Bug

3 Messes with my sessions

Warning: session_start() [function.session-start]: Cannot send session cache limiter - headers already sent (output started at /usr/local/www/data/cake/gaellinn/controllers/components/ajax_valid.php:231) in /usr/local/www/data/cake/cake/libs/session.php on line 131

Warning: Cannot modify header information - headers already sent by (output started at /usr/local/www/data/cake/gaellinn/controllers/components/ajax_valid.php:231) in /usr/local/www/data/cake/cake/libs/controller/controller.php on line 401
Posted Jan 10, 2007 by riddler
 

Comment

4 Good Idea

Garth, I have been able to test this out on the Blog (Posts) Tutorial Add page.

The tree file structure can be seen at..
http://schogini.us/articles/cakephp_ajax_validation/
I feel this is an interesting idea, it has the option to use JavaScript or no JavaScript and that can be handy.

Good Work!
Sree
ZCE
Posted Mar 31, 2007 by Sreeprakash. N
 

Comment

5 working example

Hi there,

i have tried to implement your codes.
first of all, there are several typos in the codes, those are fixed by me ..
but since now i cant get this working, do you have a working example where people can learn from and see the results?

thanks in advances

Joren
Posted Oct 30, 2007 by joren