Cascading dropdowns in CakePHP 4

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.

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.

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.

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.

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.

1 Like

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);

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.

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:

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.

@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; ?>

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

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

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.

Thanks for your reply, sir.

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

653

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.

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?

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

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” “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

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)

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.

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

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

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 :slight_smile: Thanks

You need to add the option values to the select box, not send the list of data?