CakePHP and jQuery auto-complete revisited

CakePHP 2.3
jQuery 1.10.2
jQuery UI 1.10.3

I’ve realized that my old post about jQuery auto-complete and cake is still pretty popular, but hopelessly outdated.

Therefore, I figured it would be a good time to revisit that old post and give it an update.
We’ve come so far! A lot has changed in the jQuery world
since then.

For this tutorial we will create a single auto-complete field using jQuery and jQuery UI.
Although to show off some features of CakePHP we will also create a model a controller a view and a JSON response (more on that later).

The goal is simple, we’ll have a field where we’ll type some name of a car maker. If at least one character was entered, we’ll show suggestions using jQuery UI’s auto-complete widget.

First we’ll start with the schema and some data.

Let’s create our cars table.

CREATE  TABLE `test`.`cars` (
  `id` INT NOT NULL AUTO_INCREMENT ,
  `name` VARCHAR(45) NULL ,
  `created` VARCHAR(45) NULL ,
  `modified` VARCHAR(45) NULL ,
  PRIMARY KEY (`id`) )
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_unicode_ci;

Now let’s populate it with some popular brands:

INSERT INTO `cars`
(name, created, modified)
VALUES
( 'Aston Martin', now(), now() ),
( 'Acura', now(), now() ),
( 'Audi', now(), now() ),
( 'Bentley', now(), now() ),
( 'Bmw', now(), now()),
( 'Bugatti', now(), now() ),
( 'Buick', now(), now() ),
( 'Cadillac', now(), now() ),
( 'Chevrolet', now(), now() ),
( 'Chrysler', now(), now() ),
( 'Dodge', now(), now() ),
( 'Ferrari', now(), now() ),
( 'Ford', now(), now() ),
( 'Gmc', now(), now()),
( 'Honda', now(), now() ),
( 'Hyundai', now(), now() ),
( 'Infiniti', now(), now() ),
( 'Jaguar', now(), now() ),
( 'Jeep', now(), now() ),
( 'Lamborghini', now(), now() ),
( 'Lexus', now(), now() ),
( 'Lincoln', now(), now() ),
( 'Maserati', now(), now() ),
( 'Mazda', now(), now() ),
( 'Mercedes-Benz', now(), now() ),
( 'Mitsubishi', now(), now() ),
( 'Tesla', now(), now() ),
( 'Nissan', now(), now() ),
( 'Porsche', now(), now() ),
( 'Rolls Royce', now(), now() ),
( 'Subaru', now(), now() ),
( 'Tesla', now(), now() ),
( 'Toyota', now(), now() ),
( 'Volkswagen', now(), now() ),
( 'Volvo', now(), now() )

Let’s go ahead and create a new layout for this application. It will be pretty simple, let’s do something like this (this will be a new file in View/Layout/basic.ctp):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Sample App</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="">
    <?php
      echo $this->Html->css('https://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css');
    ?>
  </head>

  <body>
  <?php echo $content_for_layout; ?>

  <!-- our scripts will be here -->
  <?php echo $scripts_for_layout; ?>
  </body>
</html>

To give some style to the auto-complete field and the “suggest” drop-down, we’ll add the CSS file form jQuery’s built-in themes.

Next we’ll create a simple controller in Controllers/CarsController.php:

<?php
  class CarsController extends AppController {

    public $layout = 'basic';

    public function index() {

    }
  }

The only thing we do differently from our standard controller setup, is specifying the layout… which matches the file name above (minus the .ctp part).
We are leaving the index() action empty for now.

And finally let’s take a look at the view in View/Cars/index.ctp:

<?php
  //let's load jquery libs from google
  $this->Html->script('https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js', array('inline' => false));
  $this->Html->script('https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js', array('inline' => false));

  //load file for this view to work on 'autocomplete' field
  $this->Html->script('View/Cars/index', array('inline' => false));

  //form with autocomplete class field
  echo $this->Form->create();
  echo $this->Form->input('name', array('class' => 'ui-autocomplete',
               'id' => 'autocomplete'));
  echo $this->Form->end();

First, we load our jQuery libs from Google. Next, notice $this->Html->script(‘View/Cars/index’, array(‘inline’ => false)); this tells CakePHP that we need to load a JavaScript file from our webroot/js/View/Cars/index.js. I recommend keeping your .js files in a similar directory structure as your .ctp files.

Because we have array(‘inline’ => false) as a second argument, our script will be included in place of the $scripts_for_layout.

This pretty much completes our CakePHP setup. We now need some code to retrieve data from our DB and some JavaScript code to act on our “#autocomplete” field.
As you’ve probably guessed, this JS code will be located in webroot/js/View/Cars/index.js:

(function($) {
  $('#autocomplete').autocomplete({
        source: "/cars/index.json"
  });
})(jQuery);

That’s it… One thing to note here is the path “/cars/index.json”. By adding the .json extension to our request URL we’ll utilize cake’s built-in JSON View and format the response as JSON (or JSONP).

Let’s take a look at that now. We will need to beef up our Controller just a little:

<?php
  class CarsController extends AppController {

    public $layout = 'basic';

    public $components = array('RequestHandler');

    public function index() {
      if ($this->request->is('ajax')) {
        $term = $this->request->query('term');
        $carNames = $this->Car->getCarNames($term);
        $this->set(compact('carNames'));
        $this->set('_serialize', 'carNames');
      }
    }
  }

One thing you’ll notice is that we’ve added a RequsetHandler component. This is the magic in CakePHP that will properly handle our request from jQuery and allow us to set our response as a JSON object. You can find out more details about how ‘_serialize’ and RequestHandler work by reading up in the manual.

It is important to note that in your routes file you’ll need to enable the parsing of extensions. (i.e. index.json).
Simply edit app/Config/routes.php and add the following line to the file:

Router::parseExtensions();

Next you see that I am getting the list of model names from our Car model in the method called getCarNames().
This is because I’m trying to follow the golden rule of MVC: “fat models, skinny controllers”.
Although it’s easy to leave all the car-name-finding logic in the controller (and not have to create a model at all!), we’ll presume good architecture here and create a model to handle our data finding needs.

Here we go (app/Model/Car.php):

<?php
  class Car extends AppModel {

    public function getCarNames ($term = null) {
      if(!empty($term)) {
        $cars = $this->find('list', array(
          'conditions' => array(
            'name LIKE' => trim($term) . '%'
          )
        ));
        return $cars;
      }
      return false;
    }
  }

I use a standard find(‘list’) method of CakePHP to get car names from our table above. The data is returned in an array formatted in a way so that becomes very easy to return as a JSON object back to our jQuery. You can see that in the controller above.
First we set a variable for the view (as you’d do for any view) and then you “serialize” it to become a JSON object.

(By creating a Car model cake automatically associated it with “cars” table. Even if I didn’t actually crate a Car model file, cake would still be able to execute basic model methods as all of our methods extend the built-in core Model. This topic is a bit more advanced and you can find out more about by studying he API or checking up on our friendly IRC channel).

In conclusion, we have everything we need to have a fully functional auto-complete using CakePHP and jQuery/jQuery UI.
If you were to type-in “f” in the input field, you’d get a list with “Ferrari” and “Ford”.

%d bloggers like this: