Use CakePHP + jQuery to build dynamic selects…

First, I can’t believe I’ve missed a whole month of posting…. damn 28 days :(

Anyway, a recent post on the bakery http://bakery.cakephp.org/articles/view/dynamic-select-boxes-with-ajax-jquery prompted me to show a slightly more accurate approach on working with the given scenario.

(I don’t mean to piggy-back on someone’s work, but I feel it deserves a little “touch-up”).

If you don’t feel like reading the other post, the basic idea is to build a dynamic select list using CakePHP + jQuery.

For this example we’ll first select a car make and then build a select list of available models using jQuery.

In order to accomplish this, first of all, the appropriate association should be established between the models:
Car hasMany CarModel

Based on that we can have two controllers:

  1. cars_controller.php
  2. car_models_controller.php

Next, of course, we’ll need some actions and views…

(The simple add/edit/etc… you could easily “bake”, so I’ll just focus on jQuery and relevant views at this point).

In CarsController we’ll add a list_models() method…

Now let’s take a look at the relevant view (list_models.ctp).
Again, here we are only focusing on the two drop-downs.

<?php $this->Html->script('views/cars/list_models.js', array('inline' => FALSE)); ?>

<?php
  echo $this->Form->input('Car.name', array('empty' => 'Select One', 'options' => $names, 'id' => 'car-name'));
?>

<div id=&quot;car-models&quot; style=&quot;display: none;&quot;>
  <?php echo $this->Form->input('CarModel.name', array('type' => 'select', 'id' => 'car-model-name')); ?>
</div>

First, we’ll load up the jQuery script, which is relevant to the view. Despite my previous conventions, I find it much easier to replicate the structure of your JS file placement exactly as you’d do for the views. With one obvious difference, that all JS goes under /webroot/js/views/some_controller/same_as_view_name.js

You’ll notice that I wrapped the second select input into a div, which is hidden by default.
This is just one approach, but you certainly could leave it visible in your UI and populate it with an:
‘empty’ => ‘Select Car First’ … just a matter of choice here, I guess.

Next, comes our cars_controller.php:
I’m only showing the “interesting” actions.

  public function list_models() {
    $this->set('names', $this->Car->find('list'));
  }

  public function get_models_ajax() {
   Configure::write('debug', 0);
   if($this->RequestHandler->isAjax()) {
     $this->set('carModels', $this->Car->CarModel->find('list',
                            array('conditions' =>
                                        array('CarModel.car_id' => $this->params['url']['carId']),
                                  'recursive' => -1)));
   }
 }

Let’s review the code a little… The list_models() method doesn’t really do anything special, it simply sets the car names to be used for the first select list in the view.

The get_models_ajax() will be called via jQuery in order to build our second select input. We are turning off debug here, so that any “extra” output does not mess with the returned data…

Yet, a side note… I am referring to SQL debug, officially produced by cake, or timestamp…
Keep the debug “on” and the resulting output (in case of errors) will be seen in the firebug console… and if you don’t have firebug, then I don’t know how to debug AJAX stuff.

(Update: had to strike that one out, since in 1.3+ this has been dramatically improved and is no longer relevant)

Also, note the $this->params['url']['carId']. This value will come from our first select list, which lists the car names with the corresponding ID’s from the database. That is because we’ve previously established a proper model association, therefore finding all the models for a given car (car_id) is no trouble at all now. (Oh, and please don’t forget to include RequestHandler in your list of required components, see the manual for more info).

Next, we still need a view for our get_models_ajax() action. The purpose of that view would be to return all the $carModels, which as you see we are setting in the controller.

Here it is, get_models_ajax.ctp:

<?php
  if(isset($carModels)) {
    echo $this->Js->object($carModels);
  }
?>

(Too much for such a simple task (view and all)?… well, respect MVC and it will not come back to bite you in the ass later.)

The view is not terribly interesting, but one thing to note is that $this->Js->object($carModels); will convert the array of data, which is returned by the find(‘list’) in the controller, into a JSON object.

Mental note… You certainly don’t have to work with JSON and any type of data can be returned back to the client, but for simple AJAX communication between the client and the server I find JSON to be most convenient format.

Alright, last, but not least let’s see the jQuery snippet that makes all the magic happen.

list_models.js

$(document).ready(function(){
  $('#car-name').live('change', function() {
    if($(this).val().length != 0) {
      $.getJSON('/cars/get_models_ajax',
                  {carId: $(this).val()},
                  function(carModels) {
                    if(carModels !== null) {
                      populateCarModelList(carModels);
                    }
        });
      }
    });
});

function populateCarModelList(carModels) {
  var options = '';

  $.each(carModels, function(index, carModel) {
    options += '<option value=&quot;' + index + '&quot;>' + carModel + '</option>';
  });
  $('#car-model-name').html(options);
  $('#car-models').show();

}

Unfortunately it would take a few more days to explain every line of code in detail, and there are quite a few jQuery tutorials our there that will do a better job of explaining it, so I hope a little googl’ing will answer any outstanding questions.
… but I do want to point out a few things.

First, we are using jQuery’s handy $.getJSON, which does a GET request to a given URL with some data and returns the results back to our client.
Remember this piece: $this->params['url']['carId']? Well, that’s exactly where the carId value is coming from… i.e. the select input value, as specified between the curly brackets. Of course, there is no point in sending empty values to the server, therefore we wrap the entire chunk of AJAX code into if($(this).val().length != 0)… this will prevent jQuery making the extra call to the server if the “empty” option is selected.

Next, we already know that the data returned from the server will be a JSON object. So, before attempting to do anything with the returned data we check for some valid/good/existing data with:
if(carModels !== null)
In this example carModels is our JSON object, which is returned by CakePHP back to jQuery.

When all said and done, we use yet another awesome tool $.each to traverse the JSON object (i.e. carModels) and build our options list.
Finally, we add the freshly built HTML options list to the contents of our second select input and display it to the user.

We are pretty much done now, but just for some more detailed Q&A you can read further, if interested.

Q. Why use .live(‘change’… instead of just .change?

A. .live is a great tool to use if you are manipulating the DOM in some way and need to work with freshly inserted element. Granted in this example it is not necessary, but I wanted to show it off anyway. Just keep in mind that this approach is available and could be a life-saver at times.

Q. Why create populateCarModelList() function?
A. I like to keep things separated as much as possible, and who knows this function might come in handy for other reasons in a more complex application.

Q. Shouldn’t the get_models_ajax() action go into the CarModels Controller ?
A. Truth be told… it should. For the sake of simplicity I kept it in the same controller as the other method, but it would be “more proper” to place it in the CarModels Controller.

Q. Why did I assign DOM ID’s to the drop down elements, doesn’t cake do that automagically?
A. It does indeed, but cake’s DOM ID’s look like SomeModelThenField. In the world of CSS it is almost an unwritten rule that ID’s most often represented as some-model-then-field… so that’s my basic goal there. Thanks to a tip from Mark Story I promise to show in an upcoming post how to override the default CamelCasedID’s with dash-separated-ones.

  • http://j4vk.com andreyv

    Great post, thanks a lot!

  • http://www.gns-bloggers.blogspot.com Hamza

    Nice tutorial!!!

  • Pingback: CakePHP : signets remarquables du 09/03/2010 au 13/03/2010 | Cherry on the...

  • Thijs

    Did you guys get it to work?

    I think i did everything as described above but the new content (carModels) does not get outputted to the hidden div.. (I made the div visible as well).

    Firebug returns ‘200 OK’ when loading ajax_get_models but then nothing happens.

  • http://teknoid.wordpress.com teknoid

    @Thijs

    Try to enable your debug temporarily and see what comes back from server.
    Also see if console.log(carModels) logs expected JSON object.

  • Pingback: Andrey Vystavkin Blog » CakePHP, draggable/droppable in jQuery

  • Pingback: Use CakePHP + jQuery to build dynamic selects… « nuts and bolts of … | Source code bank

  • http://www.24hourwebdesigner.com web design

    I like the foundation of this blog has a great variety of comments I really like it, several points of view helps in the appreciation of the subject,is very interesting and I would like learn more.

  • http://amilaudana.wordpress.com/ Amila

    I tried this with new cakephp 1.3, it was not possible to get it corrected yet. is it working properly I see error occurring at

    Notice (8): Undefined property: CarsController::$RequestHandler [APP\controllers\cars_controller.php, line 12]

  • http://teknoid.wordpress.com teknoid

    @Amila

    You need to include the RequestHandler component in your relevant controller or AppController.

  • http://www.bitesizedlanguages.com Simon

    Excellent tutorial thanks, it saved me a lot of time!

  • http://teknoid.wordpress.com teknoid

    @Simon

    Glad to help.

  • Giuseppe

    Great job, thanks! ;)

  • http://teknoid.wordpress.com teknoid

    @Giuseppe

    No problemo ;)

  • pepe

    dont forget to putin controller :

    var $helpers = array(‘Js’ => array(‘Jquery’));

  • john

    I’m trouble making this work for a HABTM relationship. I have a vendors and processes table joined by vendors_processes. Any tips?

  • john

    I’m trouble making this work for a HABTM relationship. I have a vendors and processes table joined by vendors_processes. Just to clarify, I wanted to be able to select a process and limit the vendors dropdown box to only the vendors that do that process. I’m stumped on how I can make this work with a HABTM since there is a join table vendors_processes. Any tips?

  • teknoid

    @john

    Search the join table for the correct ID.
    Once you get the data back, you’d have to make sure it is properly formatted, similar to what you’d get with find(‘list’).
    Perhaps in afterFind() of the model for the join table.

  • djstearns

    I am having troubles sending the variable back from the controller/view back to my callback function in getJSON. I’ve tried removing the “dofunction(testme)” from the JSON and replacing it with an alert (which pops up when I change the drop down), but it seems the controller function isn’t called, or the view isn’t able to pass the variable back to Js. Can anyone help?

    js script:

    $(document).ready(function(){
    $(‘#model-name’).live(‘change’, function() {
    if($(this).val().length != 0) {

    $.getJSON(‘/strainers/get_flds_ajax’,$(this).val(),//alert(‘t’)
    dofunction(testme)

    );
    }
    });
    });
    function dofunction(arr) {
    var options = ”;

    $.each(arr, function(index, arr) {
    options += ”+arr+”;
    });
    $(‘#flds-name’).html(options);


    Controller:

    public function get_flds_ajax() {
    $test = array(1,2,3,4);
    $this->set(‘testme’, $test);
    }

    View:
    Js->object($testme);
    }
    ?>

  • teknoid

    @djstearns

    I see a lot of syntax errors… now, I am not sure if it is because of copy/paste or you actually have them.
    If you can paste your snippets into some paste-bin, it would be much easier to decipher.

  • djstearns

    Sorry for littering all over your comments, but I caught several of my mistakes.

    Please use this http://pastebin.org/29779 version of pastebin, as the previous one has far too many errors (something in my copy paste was off).

    Cheers,
    Djstearns

  • hellbreak

    Good tutorial.
    I’m new to cakephp1.3. Can someone post a complete running example?
    Thank you

  • http://www.designvoid.com designvoid

    Great little tutorial, worked like a charm!
    I actually expanded it to work for a 3rd select.

    Which is where the issue is, which I am not asking you to solves, just wondered if you had any tips – it all works fine in sequence, but then if you change the 1st the 2nd changes but the 3rd remains as it was… I may we fix it before you check this, but if you have any advice that’s be great!

    • Miglos

      I’m working on a project using cakephp 2.0, and wondered if you finally got the three level selects working.
      Would you share your zipped files with me or post what you did in here?
      Thanks!

  • eddy

    after a little tweaking here and there it worked like a charm. thanks

  • http://www.psinmo.com Pedro

    I’m fairly new to cakephp, where I have to put

    if(isset($carModels)) {
    echo $this->Js->object($carModels);
    }
    Thanks

  • teknoid

    @Pedro

    It goes in the view of your corresponding action. Using example above:
    app/views/get_models_ajax.ctp

  • John

    Thanks for the tutorial, Teknoid. I’ve had many problems getting it to work with the newer versions of CakePHP and jQuery, but have managed to squash a few of the issues I’m running into.

    Problem is, there’s a pretty big one now that I cannot overcome. I have firebug running on my browser and am noticing that the jQuery doesn’t seem to be doing anything with JSON at all. I choose my “car” in the first dropdown and see that it properly calls the get_models_ajax.ctp (and also properly passes the car id)…but no JSON is created. When i var_dump() the data in get_models_ajax, I definitely see the query is running properly, and is grabbing car and model info.

    One major change I was forced to do across all scripts was to change $this->Js->whatever into $js->whatever (again, even though I’m running newest stable versions that should support this!!). I’ve also ensured to add ‘Js’ => array(‘Jquery’) to my AppController file and include the jQuery files in my default.ctp header.

    I mean, everything is working (even simple jQuery tests like div color changes) except the JSON. What might be the issue?

    Thanks again!
    John

  • teknoid

    @John
    $js->whatever() … is old syntax.
    certainly not recent 1.3

    … and since this example only uses json_encode(), you might as well use that instead (it’s faster anyway). also check out the post, which talks about using a custom JSON view to make things faster.

  • steve

    I’m using your approach for an ‘add’ form with other input fields. Works fine unless there are validation errors in any other fields. When that happens the form displays with the error message but the second dropdown has no options.

    To proceed I have to select a different option in the first dropdown (to get things working again) then make my original selections in both dropdowns (and of course fix the validation error).

    Is there a way to reset the dropdowns to their selected values when validation errors occur?

  • Dale

    if(isset($carModels)) {
    echo $javascript->object($carModels);
    }

    Sorry, the format on that last comment was messed up. This one should work.

  • teknoid

    @Dale

    It’s faster to just use json_decode()… that being said, check out the post which talks about speeding things up using a custom view object.

  • Matt

    Pretty cool tutorial, thanks. I’m not a JavaScript guru so I’m wondering if there is a way to avoid hard coding the path to the app. For example, if cake is in a sub-directory you would need a statement like

    [code]
    $.getJSON('/cake/cars/get_models_ajax',
    [/code]

    which would break if your app was moved to another directory.

    I also added a statement to hide the div if the empty option is chosen

    $(‘#car-models’).hide();

    And just as a side not, this seems like an awful lot of work just to get 1 select to update! I’ve been working with Cake for a while and this is the kind of stuff I was hoping to avoid. Maybe that is what Cake 2.0 will address?

  • teknoid

    @Matt

    You can certainly read the path from a hidden field in the view (by using $this->here, for example).

    And as far lots of work… I’m not sure that 10 lines of PHP + 20 lines of jQuery justifies “hard work” ;)

  • http://www.willis-owen.co.uk Richard Willis-Owen

    Hi – thanks for a great example.
    I’ve just done a similar thing – but I used the JsHelper and AJAX (avoiding JSON) so that no separate custom jQuery needs to be written.
    see: http://www.willis-owen.co.uk/2011/11/dynamic-select-box-with-cakephp-2-0/