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!

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/CarController.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”.

  • fly2279

    Thanks for updating this article. I’m curious as to why you didn’t just find ‘list’ to get a list of car names matching the search term instead of using the Hash lib?

    • teknoid_cakephp

      Awesome point. I kind of realized that way after I wrote the whole thing. I was debating to leave Hash in there, just to demo it… but that would totally distract from the article. So thanks for inspiring me to make it more simple.

  • euromark

    App::uses(‘Lib’, ‘Hash’); should be App::uses(‘Hash’, ‘Utility’);

    and that method should better always return an array :)

    • teknoid_cakephp

      Thanks for pointing that out, but I’ve decided to really simplify the whole thing with find(‘list’) ;)

      What’s wrong with returning a bool?

      • Daniel Hofstetter

        At least for me the “problem” is that I would expect this method to return all car names if I don’t specify any term.

      • teknoid

        Makes sense… but I’m not sure if this is the right expectation in this case. We are passing an argument to the method and act on it. Therefore, IMO, if nothing is found containing the given term, then nothing should be returned.

  • ian

    not working brother

    • teknoid

      seems to work ok ;)

    • teknoid_cakephp

      seems to work alright :)

  • Nicolas Ducrotoy

    Hello, I adapted this code for city but I have a problem

    The accent return a null value …

    Can you help me ;) ?

    Sincerely yours

  • Lokeshjain2008

    Nice article. your post helped me. I have one question how do you bake views for pages controller(for static pages)?

    • teknoid

      there are typically no actions in PagesController. and your static pages are typically just HTML and maybe some basic PHP…