Using AJAX and CakePHP to dynamically populate select form fields

Today, I struggled a bit to get a select field to be dynamically populated with values. The values are depending on what choice has been made earlier in the form in combination with CakePHP.

Before we get into things I must give credit where credit is due, this article helped me a lot, although since it was based on CakePHP 1.2 things were a bit outdated, so I had to adjust it here and there.

Time to get to the point. I’m assuming that you’re already familiar with AJAX and are just trying to integrate it nicely into your Cake app. I won’t be getting into AJAX itself, so if you’re not familiar with that, I suggest reading up on it first.

Let’s say you have a form with 2 drop down menus. For example, we would like to populate a dropdown with a list of chapters whenever a book is selected from the first dropdown. We want the chapters dropdown to get re-populated with options if the book were to change. In order to achieve this, we can use CakePHP’s built-in Ajax helper, which saves us loads of time. You can easily call the Ajax helper by putting this line in the top section of your controller:

var $helpers = array('Ajax');

That will make the Ajax helper available to all the views belonging to this controller’s actions. Now, on top of that, we would like our views to be “expecting” AJAX calls and stand by for them. To do that, you can use CakePHP’s RequestHandler component, which can be included in your controller like this:

var $components = array('RequestHandler');

Now, let’s say we would like to give users the ability to add a synopsis for a chapter of a book. Depending on what book they pick, a list of chapters for that book should be presented. Let’s assume the models are associated with the relation: Book hasMany Chapter. Now let’s take a look at what the view for such a page might look like.

<h1>Add synopsis</h1>
<?php echo $this->Form->create('Book');
echo $this->Form->input('Book.id', array('label' => 'Book', 'empty' => '-- Pick a book --'));
echo $this->Form->input('Book.chapter_id', array('label' => 'Chapter', 'empty' => '-- Pick a chapter --'));
echo $this->Form->input('Chapter.synopsis');
echo $this->Form->end('Submit synopsis');
?>

This should produce a very simple form where the user can pick a book, a chapter and write a synopsis for it. Now let’s take a look at how we’re going to populate those dropdowns. For this to happen, we’d first have to supply a list of books that the controller reads from the model and hands to the view for presentation. The controller code should then look something like this:

function synopsis() {
    $this->set('books', $this->Book->find('list');
}

This does nothing more then set the $books variable to contain all the book id’s and names and prepare them for our dropdown. By naming it ‘books’, CakePHP magic should automatically turn the Book.id form field into a dropdown containing those values. If it doesn’t, just set it manually in your view, like this:

echo $this->Form->input('Book.id', array('label' => 'Book', 'empty' => '-- Pick a book --', 'options' => $books));

Next, we need to create the element that will be used by the Ajax helper to populate the “new” select form. Most examples I found on the web were just creating a seperate view for the controller to do this, but elements were specifically made for these kind of situations where you want to just present a ‘fraction’ of code. While both methods work, I went for an element, since I might be using AJAX in multiple controllers throughout my application, so it just made more sense. So I created a simple, yet effetive element located at views/elements/ajax_dropdown.ctp that looks like this:

<?php foreach($options as $key => $val) { ?>
<option value="<?php echo $key; ?>"><?php echo $val; ?></option>
<?php } ?>

Now, to get data submitted into the element, a simple function in your controller is required. I called it ajax_chapters, but you can pretty much name it anything, whatever makes sense to you. The function needs to fetch new data that is related to the selected item in the form. My function looks like this:

function ajax_chapters() {
    // Fill select form field after Ajax request.
    $this->set('options',
        $this->Book->Chapter->find('list',
            array(
                'conditions' => array(
                    'Chapter.book_id' => $this->data['Book']['id']
                )
            )
        )
    );
    $this->render('/elements/ajax_dropdown');
}

The function will retrieve all the chapters that are tied to the book’s id. After it has done that it should not render to the ‘default’ ajax_chapters.ctp view (which we don’t have), but we tell it to specifically render using the ajax_dropdown element created earlier.

OK, so that was pretty much the preparation for what we’re actually trying to achieve here, being a dynamically populated chapter list. With all these preparations in place, it’s actually pretty simple once you know the drill. It took me some trial and error to get it working, but it pretty much comes down to two things.

  1. Make sure to load the prototype.js in your view.
  2. Use the observeField method of the Ajax helper to “observe” the Book selection.

The first one is pretty simple, just add this to the top of your view:

$this->Html->script('prototype', array('inline' => false));

The second one can be implemented at the bottom of your form with a call like this:

echo $this->Ajax->observeField('BookId', array('url' => '/book/ajax_chapters', 'update' => 'BookChapterId'));

What this does is “observe” the Book.id field for any change. When a change is detected, the ajax_chapters function is called and it’s results are passed to the Book.chapter_id field (as set in the ‘update’ attribute). In both cases, we use the field’s id names using Cake name conventions. The important part in the above code is the ‘url’ part. In my case I was working inside an admin (using routing prefixes) environment, so it was crucial to have the full url to the controller action, instead of just the action (as many examples out there demonstrate). In any case, you should always be safe when using the full path like shown above.

That is pretty much it. Whenever another book is selected, the chapters should automatically be updated in a second or so. I hope this helps some other people to get things up and running quickly.

As always, if you have any questions or problems, feel free to post a comment.

  1. Thanks for this great article :)

    But with CakePHP 1.3 the AjaxHelper is deprecated and it’s recommended to switch to the Js helper.

    Do you have any idea of how to do this with the Js helper since it doesn’t have an “observeField” method by default ?

    Thanks.

    • Hi Leimi,

      Thanks for pointing that out, I didn’t even notice that the AjaxHelper was deprecated in 1.3. I’ll go and play with the Js helper to see how that can achieve the same. If I have figured it out, I will post an update here.

  2. Hey,

    Ok I managed to handle that after some research :)
    I’m using jQuery here. You can find how to implement jQuery in the official doc (in the Js helper section).

    A few things to know to switch to jQuery :

    In the view, here is my code to replace observeField :

    $this->Js->get('#BookId')->event(
        'change',
    	$this->Js->request(
    		array('controller'=>'books','action'=>'ajax_chapters'),
    		array('update' => '#BookChapterId', 'dataExpression' => true, 'data' => '$("#BookId").serialize()')
    	)
    );

    When the BookId field changes, it calls ajax_chapters and sends the selected book id to it.

    In the ajax_chapters method, you will have to replace

    this->data['Book']['id']

    by

    $_GET['data']['Book']['id']

    Finally in the ajax_chapters function, don’t forget to set the ajax layout.:

    $this->render('/elements/ajax_dropdown', 'ajax');

    Thanks for your post that helped me a lot to get started ;)

  3. Leimi: Thanks a TON for posting your results here! I’m a cake/js noob and was so lost trying to make this work with the JS Helper.

    I dropped your code in, massaged it and it worked the first time!

    Incidently, I was using the same example of state/county/city for my trial so that helped me connect the dots! Here’s what I ended up with, for anyone else out there battling this:

    $options = array('url' => 'getcounties', 'update' => 'ExampleCountyId');
    echo $ajax->observeField('ExampleStateId', $options);
    

    becomes:

    $this->Js->get('#ExampleStateId')->event('change',
    	$this->Js->request(
    		array('controller'=>'examples',
    			'action'=>'getcounties'),
    		array('update' => '#ExampleCountyId', 
    			'dataExpression' => true, 
    			'data' => '$("#ExampleStateId").serialize()')
    	)
    );		
    

    and this:

    function getCounties() {
    	$this->set('options',
    		$this->Example->County->find('list',
    			array(
    				'conditions' => array(
    					'County.state_id' => $this->data['Example']['state_id']
    				),
    				'group' => array('County.name')
    			)
    		)
    	);
    	$this->render('/examples/ajax_result');
    }
    
    

    becomes:

    function getCounties() {
    	$this->set('options',
    		$this->Example->County->find('list',
    			array(
    				'conditions' => array(
    					'County.state_id' => $this->params['url']['data']['Example']['state_id']
    				),
    				'group' => array('County.name')
    			)
    		)
    	);
    	$this->render('/examples/ajax_result', 'ajax');
    }
    
  4. If you have Aros ACos in your Cake application give the right permissions to function ajax_chapters() {} or similar. If you doesn’t the code don’t work.

    Thanks for this code.

  5. Hi,

    In my application I have a user signup form with two dropdown lists, one for counties (static list) and one for municipalities (dynamically generated).

    When I select a county and a municipality, but leaves the username blank and click submit, Cake’s validation kicks in and say “you must enter a username”. At this point, Cake changes the municipality field from a dropdown list to a regular text field with the selected municipality id as it’s value.

    Have you seen this problem? I’m really stuck at this one. I’m using Cake 1.3.10.

    Anyway, great article – keep it up!

    Br,
    Mats

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" cssfile="">