A complete tutorial on CakePHP and AJAX forms using jQuery

in AJAX/CakePHP/jQuery/Javascript/Tutorials & Samples/Web Development

The following is a complete tutorial on how to setup a CakePHP AJAX form using jQuery. In this tutorial I'll show you how to submit a form that sends data to a controller, without page refresh using jQuery. We will also provide feeback to users after form submission and display the results once the controller has finished processing. As a bonus, I have also included AJAX model validation in this tutorial as well as integration with Twitter Bootstrap, Fontawesome and jQuery. (It’s not that I don’t like the basic cakePHP theme, it’s just that Bootstrap is so much cleaner…) As usual, you can view the sample application here and you can download the full source code here. The source code was written with CakePHP 2.5 but any version of CakePHP 2.x should work. So let’s get started.

What we will be building

AJAX forms allows you to submit a form without needing to refresh the page using popular Javascript libraries like jQuery. In this example, we’ll build a simple form with a city name. The form submits the city name to a controller action without page refresh, using native jQuery functions (native meaning, you don't need to download any extra plugins to make it work.) The action then returns the timezone for the provided city name. To make things more interesting, I only allow 6 valid city names. All other entries will result in an error. Below is a screenshot of what we will build:

screenshot-cake-jquery-ajax

Step 1 – Setup the model

The first thing we need to do is setup our model. In this tutorial, we have a model called Timezone. This model is very basic since all it has as fields is a city name. Since I am not storing anything in a database in this tutorial, the model does not use a database table. That is why I add the following line of code:

public $useTable = false; // This model does not use a database table

Additionally, I have added validation rules on the model for the city name. The city name cannot be empty and it must be one of the following cities: Montreal, Mumbai, New York, London, Paris, Sydney or Toronto. To do so, I created a new validation rule called cityIsValid(). This function checks the city name and ignores case to see if the city name matches one of my allowed cities. The following is the contents of the function cityIsValid()

	public function cityIsValid($check) {
        // $data array is passed using the form field name as the key
        // have to extract the value to make the function generic
        $value = array_values($check);
        $value = $value[0];

        if (strcasecmp($value, "Montreal") == 0) {
			return true;
		}else if (strcasecmp($value, "Mumbai") == 0) {
			return true;
		}else if (strcasecmp($value, "New York") == 0) {
			return true;
		}else if (strcasecmp($value, "London") == 0) {
			return true;
		}else if (strcasecmp($value, "Paris") == 0) {
			return true;
		}else if (strcasecmp($value, "Sydney") == 0) {
			return true;
		}else if (strcasecmp($value, "Toronto") == 0) {
			return true;
		}
		
		return false;
    }

That’s all there is to the model. The full source code for the model is presented below:

<?php
class Timezone extends AppModel {
	
    var $name = 'Timezone';
	public $useTable = false; // This model does not use a database table
 
 
    public $validate = array(
        'city'  => array(
            'empty_validation'      => array(
                'rule'      => 'notEmpty',
                'message'   => 'City name can not be left empty'
            ),
			'city_name_validation' => array(
					'rule'    => array('cityIsValid'),
					'message' => 'City name is invalid. Please enter one of the following cities: Montreal, Mumbai, New York City, London, Paris or Sydney, Toronoto'
			),
            
        )
    );  
	
	public function cityIsValid($check) {
        // $data array is passed using the form field name as the key
        // have to extract the value to make the function generic
        $value = array_values($check);
        $value = $value[0];

        if (strcasecmp($value, "Montreal") == 0) {
			return true;
		}else if (strcasecmp($value, "Mumbai") == 0) {
			return true;
		}else if (strcasecmp($value, "New York") == 0) {
			return true;
		}else if (strcasecmp($value, "London") == 0) {
			return true;
		}else if (strcasecmp($value, "Paris") == 0) {
			return true;
		}else if (strcasecmp($value, "Sydney") == 0) {
			return true;
		}else if (strcasecmp($value, "Toronto") == 0) {
			return true;
		}
		
		return false;
    }

}
?>

Step 2 – Setup the controller

The next thing that we will do is setup our controller. I have created a controller called TimezonesController.php which uses the Timezone model, the JS helper and the requestHandler component. This controller basically has two functions get_time() which displays the form and ajax_ get_time() which handles the AJAX requests. I have also overridden the beforeFilter() function so that we handle ajax requests in one area. Let’s start with the beforeFilter() function.  In beforefilter, I add an extra check for AJAX requests. Whenever an AJAX request is received, we automatically switch our layout to the AJAX layout. The AJAX layout is provided by default in the app/view/layouts folder of CakePHP. Here is the beforefilter() function:


	public function beforeFilter() {
		parent::beforeFilter();

		// Change layout for Ajax requests
		if ($this->request->is('ajax')) {
			$this->layout = 'ajax';
		}
	}

Next is the get_time() function which is basically an empty function that we simply create so that we have a view to display our form. Since it contains nothing, I will not bother showing it here. I have also modified the routes.php file so that this is the function that we load by default. This is the line in routes.php that does this.

	Router::connect('/', array('controller' => 'timezones', 'action' => 'get_time'));

	public function ajax_get_time() {
		$this->request->onlyAllow('ajax'); // No direct access via browser URL
		
		$content = '<div class="alert alert-warning" role="alert">Something unexpected occured</div>';
		
		if ($this->request->is('post')) {
			
			$this->Timezone->set($this->request->data);
			if($this->Timezone->validates()){
				$city = $this->request->data['Timezone']['city'];
				if (strcasecmp($city, "Montreal") == 0) {
					date_default_timezone_set('America/Toronto');
					$content = '<div class="alert alert-success" role="alert">The time in Montreal is : <strong>'.date('D M d Y - H:i:s ', time()).'</strong></div>';
				}else if (strcasecmp($city, "Mumbai") == 0) {
					date_default_timezone_set('Asia/Calcutta');
					$content = '<div class="alert alert-success" role="alert">The time in Mumbai is : <strong>'.date('D M d Y - H:i:s ', time()).'</strong></div>';
				}else if (strcasecmp($city, "New York") == 0) {
					date_default_timezone_set('America/New_York');
					$content = '<div class="alert alert-success" role="alert">The time in New York is : <strong>'.date('D M d Y - H:i:s ', time()).'</strong></div>';
				}else if (strcasecmp($city, "London") == 0) {
					date_default_timezone_set('Europe/London');
					$content = '<div class="alert alert-success" role="alert">The time in London is : <strong>'.date('D M d Y - H:i:s ', time()).'</strong></div>';
				}else if (strcasecmp($city, "Paris") == 0) {
					date_default_timezone_set('Europe/Paris');
					$content = '<div class="alert alert-success" role="alert">The time in Paris is : <strong>'.date('D M d Y - H:i:s ', time()).'</strong></div>';
				}else if (strcasecmp($city, "Sydney") == 0) {
					date_default_timezone_set('Australia/Sydney');
					$content = '<div class="alert alert-success" role="alert">The time in Sydney is : <strong>'.date('D M d Y - H:i:s ', time()).'</strong></div>';
				}else if (strcasecmp($city, "Toronto") == 0) {
					date_default_timezone_set('America/Toronto');
					$content = '<div class="alert alert-success" role="alert">The time in Toronto is : <strong>'.date('D M d Y - H:i:s ', time()).'</strong></div>';
				}else{
					$content = '<div class="alert alert-danger" role="alert">An Unexpected Error Occured </div>';
				}
			}else{
				$errors = $this->Timezone->validationErrors;
				$flatErrors = Set::flatten($errors);
				if(count($errors) > 0) { 
					$content = '<div class="alert alert-danger" role="alert">Could not get timezone. The following errors occurred: ';
					$content .= '<ul>';
					 foreach($flatErrors as $key => $value) {
						$content .= '<li><strong>'.$value.'</strong></li>';
					 }
					$content .= '</ul>';
					$content .= '</div>';
				}
			}

		}
		
		//set current date as content to show in view
		$this->set(compact('content')); 
	  
		//render spacial view for ajax
		$this->render('ajax_response', 'ajax');	

	}

As you can see, the first thing that I do is specify that this function should only allow AJAX requests. This prevents people from directly accessing the link and guarantees that this action will only work for AJAX requests. Then I check that the action actually came from a form post. If so, then we have work to do. Since I am using a model with no database table, I must call the validation function myself. This is done by first setting the data that I have to the model and then calling the validates function to trigger the form validation. These 2 lines take care of this:

			$this->Timezone->set($this->request->data);
			if($this->Timezone->validates()){

If the validation passes, then we simply do our processing and store our result in a variable called $content. In the case of this tutorial, we get the current time of the provided city and store it in $content. If valiation fails, then I retrieve the validation error and store it in the $content variable. Notice that I wrapped the content string in a <div>. This is done simply to make things look pretty since I am using Bootstrap and I want to display the data in an alert box. It is not necessary to do so.

The last thing that I do in the function is override the rendering of this action. Since it is an AJAX action, I inform the renderer to use a view file called ajax_response.ctp by adding the following line:

		//render spacial view for ajax
		$this->render('ajax_response', 'ajax');	

Step 3 – Setup the view and layout files

Since AJAX requests are handled on the frontend side of things, most of the work to get things working happens on the view and layout files. Let’s ump right into both.

Step 3a – update the layout file

In order for the AJAX requests to work, we will need to modify the default view located at app/views/layouts/default.ctp. As I said earlier, I am using jQuery, Bootstrap and Fontawesome in this tutorial so we will need to include all these libraries in the layout file. I do that with the following additions to the file:

		echo $this->Html->script(array(
			'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js',
			'global'
		));
		echo $this->Html->css('//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css');
		echo $this->Html->css('//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css');

As you can see, I am loading all of these files from external CDNs (Content Delivery Networks) that host these popular files. Then I add the call to the JS->writeBuffer() which writes all cached scripts and I fetch all scripts labeld as scriptBottom. <strong>If you don’t include writeBuffer in your layout or view file, none of your JS code will work</strong>

	echo $this->Js->writeBuffer(); // Write cached scripts
	echo $this->fetch('scriptBottom');  // fetch our scritBottom

Here is the CakePHP cookbook explanation of why writeBuffer is so important:

The JsHelper by default buffers almost all script code generated, allowing you to collect scripts throughout the view, elements and layout, and output it in one place. Outputting buffered scripts is done with $this->Js->writeBuffer(); this will return the buffer contents in a script tag.

Trust me, Forgetting to include writeBuffer will lead to a lot of frustrations while doing AJAX calls in CakePHP. Trust me, I know… Below is the full contents of the default layout file:

<!DOCTYPE html>
<html>
<head>
	<?php echo $this->Html->charset(); ?>
	<title>
		<?php echo $cakeDescription ?>:
		<?php echo $title_for_layout; ?>
	</title>
	<?php
		echo $this->Html->meta('icon');

		echo $this->Html->css('cake.generic');

		echo $this->fetch('meta');
		echo $this->fetch('css');
		echo $this->Html->script(array(
			'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js',
			'global'
		));
		echo $this->Html->css('//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css');
		echo $this->Html->css('//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css');
		echo $this->fetch('script');
		
	?>
</head>
<body>
	<div id="container">
		<div id="content">

			<?php echo $this->Session->flash(); ?>

			<?php echo $this->fetch('content'); ?>
		</div>
		<div id="footer">
			<?php echo $this->Html->link(
					$this->Html->image('cake.power.gif', array('alt' => $cakeDescription, 'border' => '0')),
					'http://www.cakephp.org/',
					array('target' => '_blank', 'escape' => false, 'id' => 'cake-powered')
				);
			?>
			<p>
				<?php echo $cakeVersion; ?>
			</p>
		</div>
	</div>
<?php
	echo $this->Js->writeBuffer(); // Write cached scripts
	echo $this->fetch('scriptBottom');  // fetch our scritBottom
	echo $this->element('sql_dump');  // dump the SQL queries
?>
</body>
</html>

Step 3a – create your view files

Get_time.ctp

Now we can create our view files. The first and most important view that we will create is get_time.ctp. Before explaining the important parts, let’s look at the entire file:

<h2>CakePHP City Time Retriever</h2>
<p>The form below allows you to enter the name of a city and get its current local time through AJAX using jQuery. You can view the tutorial for the setup <a href="http://miftyisbored.com/a-complete-tutorial-on-cakephp-and-ajax-forms-using-jquery/">here </a> </p>
<div id="cityUpdate"></div>
<?php
$data = $this->Js->get('#TimezoneForm')->serializeForm(array('isForm' => true, 'inline' => true));
$this->Js->get('#TimezoneForm')->event(
	  'submit',
	  $this->Js->request(
		array('controller' => 'timezones','action' => 'ajax_get_time'),
		array(
				'update' => '#cityUpdate',
				'data' => $data,
				'async' => true,    
				'dataExpression'=>true,
				'method' => 'POST',
				'before' => "$('#loading').fadeIn();$('#citySubmit').attr('disabled','disabled');",
				'complete' => "$('#loading').fadeOut();$('#citySubmit').removeAttr('disabled');",
			)
		)
	);
?>

<?php
echo $this->Form->create('Timezone', array(
	'id' => 'TimezoneForm',
	'controller' => 'timezones',
	'action' => 'ajax_get_time',
	'class' => '',
	'role' => 'form',
));
echo $this->Form->input('city', array('label' => 'Enter the city name', 'placeholder' => 'Enter the name of the city' )); 
echo $this->Form->submit('Get City Time', array('title' => 'Get Time', 'id'=>'citySubmit') );  
echo $this->Form->end();

?>
<div id="loading" style="display: none;"><div class="alert alert-info" role="alert"><i class=" fa fa-spinner fa-spin"></i> Please wait...</div></div>

<p>Enter any of the following cities in the text-field above to get their current local time: 
<ul>
	<li>London</li>
	<li>Montreal</li>
	<li>Mumbai</li>
	<li>New York</li>
	<li>Paris</li>
	<li>Sydney</li>
	<li>Toronto</li>
</ul>
</p>

The first part is creating the form

<?php
echo $this->Form->create('Timezone', array(
	'id' => 'TimezoneForm',
	'controller' => 'timezones',
	'action' => 'ajax_get_time',
	'class' => '',
	'role' => 'form',
));
echo $this->Form->input('city', array('label' => 'Enter the city name', 'placeholder' => 'Enter the name of the city' )); 
echo $this->Form->submit('Get City Time', array('title' => 'Get Time', 'id'=>'citySubmit') );  
echo $this->Form->end();

?>

This is basically a standard form with a submit button. The important part of this form is that I provide an ID for form and the submit button. I gave them clever names. The submit button is called citySubmit  and the form is called TimezoneForm. These IDs will be required later when we setup our jQuery AJAX conditions.

I also add 2 important div elements: one with the ID loading and other with the ID cityUpdate. loading will be displayed while we are waiting for the AJAX function to finish. That is why it’s initially hidden. cityUpdate is the div that we will update once we receive our AJAX update. Notice that the content of the loading div uses the fontawesome spinner so as to give feedback to users that something is happening. That is why I included fontawesome in the layout file J

<div id="cityUpdate"></div>
<div id="loading" style="display: none;"><div class="alert alert-info" role="alert"><i class=" fa fa-spinner fa-spin"></i> Please wait...</div></div>

Now let’s get into the Javascript call, wich is shown here and explained below:

<?php
$data = $this->Js->get('#TimezoneForm')->serializeForm(array('isForm' => true, 'inline' => true));
$this->Js->get('#TimezoneForm')->event(
	  'submit',
	  $this->Js->request(
		array('controller' => 'timezones','action' => 'ajax_get_time'),
		array(
				'update' => '#cityUpdate',
				'data' => $data,
				'async' => true,    
				'dataExpression'=>true,
				'method' => 'POST',
				'before' => "$('#loading').fadeIn();$('#citySubmit').attr('disabled','disabled');",
				'complete' => "$('#loading').fadeOut();$('#citySubmit').removeAttr('disabled');",
			)
		)
	);
?>

Essentially, this block of code tells jQuery to wait for the submission of the TimezoneForm form element. Once the form is submitted, the data from the form is serialized and then submitted to the Timezone controller and the action ajax_get_time through a POST method. jQuery is also told that once the action is completed, jQuery should update the cityUpdate div and that before submission, the loading div should fadein and that once the AJAX request is completed, the loading div should fadeout.

Ajax_response.ctp

The view file ajax_get_time.ctp does not exist. Instead, we use a view file called ajax_response.ctp which can be used for all Ajax response. The content of ajax_response.ctp is shown below:

<?php echo $content; ?>

As you can see, this file simply outputs that output of the content from the controller.

Conclusion

That’s all there is to setting up an AJAX form in CakePHP using jQuery. Happy baking…

Tags:

Mifty Yusuf is a Montreal-based software developer who enjoys playing with new web technologies as well as comic books and illustrations. He beleives that, no matter what the question is, the answer is always Batman!

11 Comments

  1. Thanks a lot forma this tutoriales, i really apreciate it.

    Keep on doing cool stuf like this plz!

    • Hi Rahul. Thanks. I plan on creating more tutorials. I just been a bit busy these days. I also plan to focus more on Laravel. It’s a really good framework that offers some serious improvements on CakePHP.

Leave a Reply

Your email address will not be published.

*

Latest from AJAX

Go to Top