# Cascading dropdowns in CakePHP 4

**URL:** <https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160>\
**Category:** Need Help\
**Created:** [January 23, 2020, 10:23pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160 "2020-01-23T22:23:52Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![bramblepuss](https://avatars.discourse-cdn.com/v4/letter/b/f6c823/32.png) [@bramblepuss](https://discourse.cakephp.org/u/bramblepuss)\
**Post date:** [January 23, 2020, 10:23pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/1 "2020-01-23T22:23:52Z")

</div>

Despite my very rusty coding skills, I’ve been working to re-write an old CakePHP 2 application in CakePHP 4. I’m almost done – hurrah! – and while it’s not the prettiest code, it works. The only outstanding issue I’m having is writing the code for cascading dropdowns. Many years ago, I followed [this brilliant tutorial](http://www.marnienickelson.com/dynamic-drop-down-menus-with-cakephp-2-x/), helped by the fact that the author (a real, bonafide developer – unlike me!) shared my frustration in trying to follow others available at the time.

Unfortunately, that doesn’t work anymore with CakePHP 4. Nor do the five others I would have linked to, except new forum users can only include two links in their posts. I’m aware that there’s an AJAX plugin but it’s much too complicated for simpletons like me, and the documentation and example code is also out-of-date for CakePHP 4.

I decided the best option might be to try and ‘upgrade’ the code in [this very, very simple example](https://gist.github.com/taleeb35/66abaae65ccd2d01a2c1) that could be entirely idiot-proof, if only it worked. As such, I’ve spent the last week or so trying to do exactly that – but I’ve hit a wall. My code is currently as follows:

**In my Countries controller:**

```
public function initialize(): void {
    parent::initialize();
    $this->loadComponent('RequestHandler');
}

public function select() {
    $countries = $this->Countries->find('list');
    $states = array();
    foreach ($countries as $country_id => $name) {
        $states = $this->Countries->States->findByCountryId($country_id);
        break;
    }
    $this->set(compact('countries', 'states'));
}

public function stateslist() {  
    if (!$this->request->is('ajax')) {
        throw new MethodNotAllowedException();
    }
    $id = $this->request->getQuery('id');
    if (!$id) {
        throw new NotFoundException();
    }
    $states = $this->Countries->States->find('list', array(
        'conditions' => array('country_id' => $id)
    ));
    $this->set(compact('states'));
}

```

**And in my select.php (Countries) template:**

```
<?= $this->Form->create() ?>
<fieldset>
    <legend><?php echo __('Countries and States');?></legend>
    <?php
        $url = $this->Url->build(['controller' => 'countries', 'action' => 'states_list', 'ext' => 'json']);
        $empty = count((array)$states) > 0 ? __('Please Select') : array('0' =>__('No States Available'));
        echo $this->Form->control('country_id', array('id' => 'countries', 'rel' => $url, 'empty' => 'Choose Country'));
        echo $this->Form->control('state_id', array('id' => 'states', 'empty' => $empty));
    ?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>

<script type="text/JavaScript">
    $(document).ready(function(){
        //$("#states").attr("disabled","disabled");
        $("#countries").change(function(){
            //$("#states").attr("disabled","disabled");
            $("#states").html("<option>Please wait...</option>");
            var id = $("#countries option:selected").attr('value');
            $.get("/countries/stateslist", {id:id}, function(data){
                $("#states").removeAttr("disabled");
                $("#states").html(data);
            });
        });
    });
</script>

```

From what I can tell, the issue appears to be that the request isn’t being processed by the controller. I think it might be because I need to enable routing for the JSON extension, because right now, the request is being passed to the controller, but the controller throws an error because it can’t find a stateslist.php template (whereas, I think, it shouldn’t need one – it should process the AJAX as it is). Of course, there could be (and probably are) other issues with my hacked together code, but I’m tackling one problem at a time here.

Regrettably, I am at a total loss on how to do this in CakePHP 4. There is plenty of documentation on doing this in previous versions, and many answered questions on StackOverflow etc. (that often contradict one another, as so many things have been deprecated/replaced/upgraded over Cake versions). The bigger issue is that the CakePHP 4 cookbook also doesn’t appear to have been updated, because some the examples I try to follow (e.g. on the Routing and Views pages) no longer seem to be applicable.

With that in mind: is this my (only) problem, and could anyone please point me in the direction of how to fix it? A quick Google search shows that cascading/dependent dropdowns in CakePHP seem to have been a huge challenge for many people over the years. It would be ideal if I can get this code to become an idiot-proof example for others using CakePHP 4.

---

<div class="post-metadata">

**Author:** ![Zuluru](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/zuluru/32/1230_2.png) [@Zuluru](https://discourse.cakephp.org/u/Zuluru)\
**Post date:** [January 24, 2020, 1:21am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/2 "2020-01-24T01:21:07Z")

</div>

If it’s looking for a template, that’s because it needs to know how to format the output. Does the error message say where it can’t find that file? Knowing the exact path will help to understand what type of output it thinks it’s supposed to be generating, which in turn will help to diagnose exactly where the problem is.

---

<div class="post-metadata">

**Author:** ![bramblepuss](https://avatars.discourse-cdn.com/v4/letter/b/f6c823/32.png) [@bramblepuss](https://discourse.cakephp.org/u/bramblepuss)\
**Post date:** [January 24, 2020, 11:27pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/3 "2020-01-24T23:27:15Z")

</div>

The error is from Cake\View\Exception\MissingTemplateException; “the view for _CountriesController::stateslist()_ was not found.”

I didn’t think it would need a view because the purpose of this controller action is simply to serve data. To try and rectify this, I added:

`return $this->response;`

to the last line of the “public function stateslist() {” function.

Now I get no error, but the dropdown still doesn’t update. I’m guessing this means there’s an error either in my database queries or in my AJAX – i.e. the data isn’t being requested in the controller function, or is being requested but isn’t being passed back. I’d really appreciate suggestions on where the errors in my code might be.

---

<div class="post-metadata">

**Author:** ![Zuluru](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/zuluru/32/1230_2.png) [@Zuluru](https://discourse.cakephp.org/u/Zuluru)\
**Post date:** [January 24, 2020, 11:47pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/4 "2020-01-24T23:47:43Z")

</div>

By using `return $this->response;`, you’re telling Cake that the response object already has everything it needs. If you haven’t put any output into that (e.g. via `withStringBody` or the like), then there will be no output. When you don’t return a response object, Cake tries to render a template for you, and the template it will try to use will depend on the request type (e.g. Ajax, JSON, CSV, etc.). I was hoping / expecting that you’d get an error that tells you specifically what file it’s looking for (e.g. `src/Template/Countries/stateslist.ctp` vs `src/Template/Countries/json/stateslist.ctp`); that would provide insight into whether your request is being interpreted correctly, and give direction on what exactly you need to do to best remedy the situation.

---

<div class="post-metadata">

**Author:** ![bramblepuss](https://avatars.discourse-cdn.com/v4/letter/b/f6c823/32.png) [@bramblepuss](https://discourse.cakephp.org/u/bramblepuss)\
**Post date:** [January 24, 2020, 11:57pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/5 "2020-01-24T23:57:22Z")

</div>

Thanks so much for the quick reply. Apologies that I wasn’t clear – the missing template exception is for /templates/Countries/stateslist.php.

I can now see (by echo’ing the $id var in the Controller) that the country is being successfully passed to the Countries controller when the user selects it. It seems the database query to get the list of states is working too. So, I think the issue is how to format the output so that Cake renders the correct template and passes the result back.

---

<div class="post-metadata">

**Author:** ![Zuluru](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/zuluru/32/1230_2.png) [@Zuluru](https://discourse.cakephp.org/u/Zuluru)\
**Post date:** [January 25, 2020, 12:30am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/6 "2020-01-25T00:30:27Z")

</div>

Presumably, then, you just need to add the specified template and the code in it to loop through the query results and build the HTML that you need in the browser. And also add `$this->viewBuilder()->setLayout('ajax');` in your controller; the `ajax` layout adds no markup to whatever comes from your template, which is definitely the desired result here.

---

<div class="post-metadata">

**Author:** ![bramblepuss](https://avatars.discourse-cdn.com/v4/letter/b/f6c823/32.png) [@bramblepuss](https://discourse.cakephp.org/u/bramblepuss)\
**Post date:** [January 25, 2020, 12:49am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/7 "2020-01-25T00:49:20Z")

</div>

Might there be another way of doing this that doesn’t require the specified template? For example, if I add this code to the bottom of the function:

```
$this->set(compact('states'));
$this->RequestHandler->respondAs('json');
$this->response = $this->response->withType('json');
$this->autoRender = false; 
json_encode($states);
echo json_encode($states);

```

I do get the desired output (kind of) – the following output is echo’d (as an example, I selected Australia, which has two ‘states’ in the database as an example):

```
{
    "New South Wales": "New South Wales",
    "Western Australia": "Western Australia"
}

```

I think the only outstanding problem is that this isn’t in the format required in the AJAX, i.e. this bit:

```
        $.get("/countries/stateslist", {id:id}, function(data){
        $("#states").removeAttr("disabled");
        $("#states").html(data);
```

---

<div class="post-metadata">

**Author:** ![bramblepuss](https://avatars.discourse-cdn.com/v4/letter/b/f6c823/32.png) [@bramblepuss](https://discourse.cakephp.org/u/bramblepuss)\
**Post date:** [January 25, 2020, 12:57am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/8 "2020-01-25T00:57:19Z")

</div>

I did it! By some miracle. I can’t thank Zuluru enough for their help! I’m going to fix some other bugs, test it a little more, and then write a step-by-step guide for anyone else who’s battling to get this done.

---

<div class="post-metadata">

**Author:** ![Zuluru](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/zuluru/32/1230_2.png) [@Zuluru](https://discourse.cakephp.org/u/Zuluru)\
**Post date:** [January 25, 2020, 1:25am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/9 "2020-01-25T01:25:46Z")

</div>

DO NOT `echo json_encode($states);`. Controllers should never echo output. It interferes with setting HTTP headers on the response, which are required for cookies, etc. Instead I think you can replace your code above (all six lines, from `$this->set` up to and including `echo`) with:

```auto
return $this->response
    ->withType('json')
    ->withStringBody(json_encode($states));

```

Alternately, it should be possible to set things up to automatically recognize and render JSON by using a `.json` suffix on your URL, but that might require other changes. In such an instance, you’d leave your `$this->set` in place, and remove all the other lines there, and other code would take care of encoding it and adding it to the response.

---

<div class="post-metadata">

**Author:** ![bramblepuss](https://avatars.discourse-cdn.com/v4/letter/b/f6c823/32.png) [@bramblepuss](https://discourse.cakephp.org/u/bramblepuss)\
**Post date:** [January 25, 2020, 2:33am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/10 "2020-01-25T02:33:10Z")

</div>

@Zuluru – no, I only used `echo json_encode($states)` when debugging to make sure it was populating `$states` as expected. In any case, here’s the ‘final’ code for anyone else who might find it helpful. Is it perfect? Probably not. Does it work? Yes.

**In Countries controller:**

```
public function select() {
    $countries = $this->Countries->find('list');
    $states = array();
    foreach ($countries as $country_id => $name) {
        $states = $this->Countries->States->findByCountryId($country_id);
        break;
    }
    $this->set(compact('countries', 'states'));
}

public function stateslist() {  
    if ($this->request->is('ajax') && null !== $this->request->getQuery('id')) {
            $states = $this->Countries->States->find('list', array('conditions' => array('country_id' => $this->request->getQuery('id'))));
            $this->set(compact('states'));
    } else {
        return $this->redirect(['action' => 'index']);
    }
}

```

**In /templates/Countries/select.php:**

```
<?= $this->Form->create() ?>
<fieldset>
    <legend><?php echo __('Countries and States');?></legend>
    <?php
        $url = $this->Url->build(['controller' => 'countries', 'action' => 'stateslist', 'ext' => 'json']);
        $empty = count((array)$states) > 0 ? __('Please Select') : array('0' =>__('No States Available'));
        echo $this->Form->control('country_id', array('id' => 'countries', 'rel' => $url, 'empty' => 'Choose Country'));
        echo $this->Form->control('state_id', array('id' => 'states', 'empty' => $empty));
    ?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>

<script type="text/JavaScript">
    $(document).ready(function(){
        $("#states").attr("disabled","disabled");
        $("#countries").change(function(){
            $("#states").attr("disabled","disabled");
            $("#states").html("<option>Please wait...</option>");
            var id = $("#countries option:selected").attr('value');
            $.get("/countries/stateslist/", {id:id}, function(data){
                $("#states").removeAttr("disabled");
                $("#states").html(data);
            });
        });
    });
</script>

```

**In /templates/Countries/statelist.php:**

```
<?php foreach($states as $key => $value): ?>
<option value="<?php echo $key; ?>"><?php echo $value; ?></option>
<?php endforeach; ?>
```

---

<div class="post-metadata">

**Author:** ![Zuluru](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/zuluru/32/1230_2.png) [@Zuluru](https://discourse.cakephp.org/u/Zuluru)\
**Post date:** [January 25, 2020, 3:51am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/11 "2020-01-25T03:51:04Z")

</div>

So you did opt for the template. It’s that or parse JSON to build the HTML on the client side, really.

---

<div class="post-metadata">

**Author:** ![Foggies](https://avatars.discourse-cdn.com/v4/letter/f/a183cd/32.png) [@Foggies](https://discourse.cakephp.org/u/Foggies)\
**Post date:** [June 21, 2020, 3:45am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/12 "2020-06-21T03:45:48Z")

</div>

Hi, I’ve followed the code but it when I select the country from the list, the list of the state is not generated but stuck to “please wait”. How can I solve these issues? thanks

 ![654](https://canada1.discourse-cdn.com/flex029/uploads/cakephp/original/2X/d/d099331e53057beb67dda1332f98d3acdd317edd.jpeg)

---

<div class="post-metadata">

**Author:** ![Zuluru](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/zuluru/32/1230_2.png) [@Zuluru](https://discourse.cakephp.org/u/Zuluru)\
**Post date:** [June 21, 2020, 3:56am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/13 "2020-06-21T03:56:58Z")

</div>

Check your browser console, seems fairly likely that there’s a JavaScript error either in sending the request or receiving the results. And check your server logs, most specifically the Cake error log. There are so many possible causes for this, you need to narrow down the source of the problem.

---

<div class="post-metadata">

**Author:** ![Foggies](https://avatars.discourse-cdn.com/v4/letter/f/a183cd/32.png) [@Foggies](https://discourse.cakephp.org/u/Foggies)\
**Post date:** [June 21, 2020, 4:25am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/14 "2020-06-21T04:25:37Z")

</div>

Thanks for your reply, sir.

I’ve checked the browser console and it shows error: 404 not found.

![653](https://canada1.discourse-cdn.com/flex029/uploads/cakephp/original/2X/e/e055bd192c66ab938e020f7c84c51aac14b8df46.jpeg)

Basically I’ve loaded the jquery in **head** in the default layout. Is it caused by jquery or others? The log file have no error.

---

<div class="post-metadata">

**Author:** ![Zuluru](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/zuluru/32/1230_2.png) [@Zuluru](https://discourse.cakephp.org/u/Zuluru)\
**Post date:** [June 21, 2020, 4:55pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/15 "2020-06-21T16:55:35Z")

</div>

What do your Apache access logs say? Is this request being received by the server? What happens if you try to view `http://localhost/countries/stateslist/?id=1` in your browser?

---

<div class="post-metadata">

**Author:** ![Foggies](https://avatars.discourse-cdn.com/v4/letter/f/a183cd/32.png) [@Foggies](https://discourse.cakephp.org/u/Foggies)\
**Post date:** [June 22, 2020, 12:03am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/16 "2020-06-22T00:03:33Z")

</div>

when accessing the `http://localhost/countries/stateslist/?id=1` it shows Object not found!

 ![555](https://canada1.discourse-cdn.com/flex029/uploads/cakephp/original/2X/5/503c9c44966283b83aa7e24c37f7a5acdd3438b6.jpeg)

and the apache access log shows:

::1 - - [22/Jun/2020:07:18:06 +0800] “GET /countries/stateslist/?id=1 HTTP/1.1” 404 1223 “[http://localhost/cake4/countries/select](http://localhost/cake4/countries/select)” “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36”

in the stateslist, is redirect to the index which will lead to [http://localhost/cake4/countries/select](http://localhost/cake4/countries/select)

```
public function stateslist() {  
if ($this->request->is('ajax') && null !== $this->request->getQuery('id')) {
        $states = $this->Countries->States->find('list', array('conditions' => array('country_id' => $this->request->getQuery('id'))));
        $this->set(compact('states'));
} else {
    return $this->redirect(['action' => 'index']);
}

```

}

then if I changed the js in stateslist.php to /cake4/… it still errors.

```
    <script type="text/JavaScript">
    $(document).ready(function(){
        $("#states").attr("disabled","disabled");
        $("#countries").change(function(){
            $("#states").attr("disabled","disabled");
            $("#states").html("<option>Please wait...</option>");
            var id = $("#countries option:selected").attr('value');
            $.get("/cake4/countries/stateslist/", {id:id}, function(data){
                $("#states").removeAttr("disabled");
                $("#states").html(data);
            });
        });
    });
</script>

```

my db table:

countries (id,name)  
states (id,name)

---

<div class="post-metadata">

**Author:** ![Zuluru](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/zuluru/32/1230_2.png) [@Zuluru](https://discourse.cakephp.org/u/Zuluru)\
**Post date:** [June 22, 2020, 12:28am UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/17 "2020-06-22T00:28:44Z")

</div>

Looks like you’re running Cake in a subfolder of your webroot, that generally requires some configuration changes which can be fiddly. I can never remember exactly what needs to be done to solve that, sorry.

Don’t have any idea why it would redirect from `.../stateslist` to `.../select`.

You’ll need to sort this all out, one way or another, such that you get something meaningful from that URL, before you can start using that URL in Ajax calls.

---

<div class="post-metadata">

**Author:** ![josser](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/josser/32/1899_2.png) [@josser](https://discourse.cakephp.org/u/josser)\
**Post date:** [June 26, 2020, 6:03pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/18 "2020-06-26T18:03:02Z")

</div>

Foggies, did you ever manage to get this issue resolved? I am encountering the same issue.

---

<div class="post-metadata">

**Author:** ![josser](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.cakephp.org/josser/32/1899_2.png) [@josser](https://discourse.cakephp.org/u/josser)\
**Post date:** [June 26, 2020, 6:45pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/19 "2020-06-26T18:45:39Z")

</div>

I figured it out. There was some inconsistencies (includes/excluding "s"s) in the names used in the scripts provided above.

---

<div class="post-metadata">

**Author:** ![Foggies](https://avatars.discourse-cdn.com/v4/letter/f/a183cd/32.png) [@Foggies](https://discourse.cakephp.org/u/Foggies)\
**Post date:** [June 29, 2020, 11:05pm UTC](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160/20 "2020-06-29T23:05:26Z")

</div>

I’m still unable to solve the issues. Great to know that you solve the problem. Thank for the hint. Would you mind to share your code? it will be helpful 🙂 Thanks

[Next page](https://discourse.cakephp.org/t/cascading-dropdowns-in-cakephp-4/7160.md?page=2)
