I wanted to learn about using AJAX to submit a form by following http://miftyisbored.com/a-complete-tutorial-on-cakephp-and-ajax-forms-using-jquery/ and I couldn't get the examples to work because CakePHP 3 has some significant (and better) changes.
Below is my attempt at the above linked tutorial for CakePHP. I have made a number of changes from the original:
- I have moved the timezone return code to the src/Form/TimezoneForm.php ( this is because apparently you want thin controllers and fat models )
- Removed deprecated CakePHP 2.x code
- Added inList validation to check the cities exist
I installed CakePHP 3 with composer and then ran it using bin/cake server
This is what the end result looked like after I made all the edits to bring it forward to CakePHP 3
In CakePHP 2.x you if a model didn't have an underlying database table associated you did the following to tell the Model it was sans table.
<?php App::uses('AppModel', 'Model'); class Timezone extends AppModel { // cake 2.x $useTable = false;
In CakePHP 3 if there is no backing database table then this is known as having a "Modelless Form"
Layout
<?php // src/Template/Layout/timezone.ctp use Cake\Core\Configure; $cakeDescription = "My Cake Description"; $cakeVersion = Configure::version(); ?> <!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'); ?> <?= $this->Html->css('base.css') ?> <?= $this->Html->css('cake.css') ?> <?php echo $this->fetch('meta'); echo $this->fetch('css'); echo $this->Html->script([ 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js', 'global' ], [ 'block' => 'scriptBottom' ] ); 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'); ?> </head> <body> <div id="container"> <div id="content"> <?= $this->Flash->render() ?> <?= $this->fetch('content') ?> <?php // Append into the 'script' block. $this->Html->scriptStart(['block' => true]); echo "console.log('I am in the JavaScript');"; $this->Html->scriptEnd(); ?> </div> </div> <div style="margin-left: 40px;"> <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') ); ?> <span style="font-size: .7em;"><?php echo $cakeVersion; ?></span> </footer> </div> <?php // echo $this->Js->writeBuffer(); // Write cached scripts echo $this->fetch('scriptBottom'); // fetch our scritBottom echo $this->fetch('script'); ?> </body> </html>
TimezoneForm
<?php // src/Form/TimezoneForm.php namespace App\Form; use Cake\Form\Form; use Cake\Form\Schema; use Cake\Validation\Validator; use Cake\Utility\Hash; use Cake\Log\LogTrait; use Cake\Utility\Inflector; class TimezoneForm extends Form { use LogTrait; protected $cities = [ [ 'city' => 'London', 'tz' => 'Europe/London' ], [ 'city' => 'Montreal', 'tz' => 'America/Toronto' ], [ 'city' => 'Mumbai', 'tz' => 'Asia/Calcutta' ], [ 'city' => 'New York', 'tz' => 'America/New_York' ], [ 'city' => 'Paris', 'tz' => 'Europe/Paris' ], // [ // 'city' => 'Sydney', // 'tz' => 'Australia/Sydney' // ], [ 'city' => 'Toronto', 'tz' => 'America/Toronto' ], [ 'city' => 'Melbourne', 'tz' => 'Australia/Melbourne' ] ]; protected function _buildSchema(Schema $schema) { return $schema->addField('city', 'string'); // ->addField('email', ['type' => 'string']) // ->addField('body', ['type' => 'text']); } public function get_cities() { $this->log($this->cities); return Hash::extract($this->cities, '{n}.city'); } public function test_func() { return "my jimmy"; } public function return_tz($city) { foreach ($this->cities as $k => $v) { if (strcasecmp($city, $v['city']) == 0) { date_default_timezone_set($v['tz']); $content = '<div class="alert alert-success" role="alert">The time in ' . $v['city'] . ' is : <strong>' . date('D M d Y - H:i:s ', time()) . '</strong></div>'; return $content; } } $ret = $form->test_func(); $content = '<div class="alert alert-danger" role="alert">An Unexpected ' . $ret . ' Error Occured </div>'; return $content; } protected function _buildValidator(Validator $validator) { return $validator->add('city', 'inList', [ 'rule' => ['inList', $this->get_cities(), true], 'message' => 'Not in list' ]); } protected function _execute(array $data) { // Send an email. return true; } }
TimezoneController
<?php // src/Controller/TimezoneController.php namespace App\Controller; use App\Controller\AppController; use Cake\Event\Event; use App\Form\TimezoneForm; // specify your form class here otherwise it won't be found //use Cake\Log\Log; use Cake\Utility\Hash; class TimezoneController extends AppController { public function beforeFilter(Event $event) { parent::beforeFilter($event); // Change layout for Ajax requests if ($this->request->is('ajax')) { $this->viewBuilder()->layout('ajax'); // src/Template/Layout/ajax.ctp } $this->viewBuilder()->layout('timezone'); // src/Template/Layout/timezone.ctp } public function getTime() { // no longer model $useTable = false; // model-less forms use src/Form/TimezoneForm.php $timezone = new TimezoneForm(); $city_list = $timezone->get_cities(); $this->set(compact('timezone', 'city_list')); } public function ajaxGetTime() { $this->request->allowMethod('ajax'); // No direct access via browser URL $content = '<div class="alert alert-warning" role="alert">Something unexpected occured</div>'; $form = new TimezoneForm(); $this->log($this->request->data); if ($this->request->is('post')) { if ($form->validate($this->request->data)) { $city = $this->request->data['city']; $content = $form->return_tz($city); } else { $errors = $form->errors(); $flatErrors = Hash::flatten($errors); // flatten the error array so you can loop through it easier // // $errors is: // ( // [city] => Array // ( // [inList] => Not in list // ) // // ) // $errors becomes: // ( // [city.inList] => Not in list // ) 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')); } }
View
<!-- src/Template/Timezone/get_time.ctp --> <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 of CakePHP 2.x <a href="http://miftyisbored.com/a-complete-tutorial-on-cakephp-and-ajax-forms-using-jquery/">here </a> </p> <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> <div id="cityUpdate"></div> <?php echo $this->Form->create( $timezone, [ 'url' => [ 'action' => 'ajax_get_time' ], 'name' => 'TimezoneForm', 'data-update' => 'cityUpdate', // the id of the div we want to return data to 'data-loading' => 'loading' // the id of the div we want to display the loading spinner ]); 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(); ?> <p></p> <p>Enter any of the following cities in the text-field above to get their current local time: <?php echo $this->Html->nestedList($city_list); ?> </p> <?php //debug($timezone); ?>
JQuery
// webroot/js/global.js $( document ).ready(function() { console.log( "ready!" ); $('form').submit( function(e) { e.preventDefault(); var form = $(this); var submit_button = $(form).find('input[type=submit]'); var loading = form.attr('data-loading'); var update = form.attr('data-update'); var form_data = form.serialize(); var url = form.attr('action'); console.log(form_data); $.ajax({ url: url, type: 'POST', dataType: 'json', data: form_data, beforeSend: function(){ $('#loading').fadeIn(); submit_button.attr('disabled','disabled'); $('input[type=text]').blur(); }, complete: function(){ $('#' + loading).fadeOut(); submit_button.removeAttr('disabled'); }, success: function(data) { console.log("cityUpdate"); $('#' + update ).fadeIn().html(data.content); }, error: function(data){ console.log(data); } }); }); });
Change the default route to hook / to the get_time action:
// config/routes.php Router::scope('/', function (RouteBuilder $routes) { # connect / to /timezone/get_time $routes->connect('/', array('controller' => 'timezone', 'action' => 'get_time')); /** * Here, we are connecting '/' (base path) to a controller called 'Pages', * its action called 'display', and we pass a param to select the view file * to use (in this case, src/Template/Pages/home.ctp)... */ # comment this # $routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
Whats the global.js for? its not used in the views?
Global.js replaces the default post action with an ajax post and and takes the reply and updates the page. In this case I included it from template.ctp