Simple Dependent Dropdown

Working out this dependent dropdown which is different from the examples out there. In the examples such as Creating a Dependent Dropdown From Scratch in Yii2 | Wiki | Yii PHP Framework the 2nd drop down is built by issuing a post with onchange to retrieve the set of values for the 2nd drop down. In my case, all I need is to change the value passed to the where clause (of the find method) of the 2nd drop down.

Here is my script:

First dropdown - did a crud modification of it - probably all wrong! idtsvalue is the id I am trying to provide.

    <?= $form->field($model, 'tsanchor')->dropDownList($options,[ 'prompt' => '', 'style' => 'width: auto;',
        'onchange' => '$(this).val(), function( data ) { $( "select#idtsvalue" ).html( data ); };'
     ]) ?>

Dependent dropdown

    <?php echo $form->field($model, 'tsvalue')->dropDownList(ArrayHelper::map(Refdetail::find()->innerjoin('refmaster', 'refmaster.id = refdetail.refm_id')
                                              ->where(['rname' => 'REFTABLENAME'])
                                              ->orderBy(['rsort'=>SORT_ASC, 'rvalue'=>SORT_ASC])
                                              ->all(), 'rcode', 'rvalue'),[ 'prompt' => '', 'style' => 'width: auto;'], ['id'=>'idtsvalue']); ?>

Where I have REFTABLENAME I want to dynamically change to the selected value in the 1st drop down. Not sure if this is a correct/viable approach.

You want to query the database or just want to modify dropdown list based on selection using jquery?

Query the database, by passing the where clause value from the 1st dropdown is what I am hoping to do.

is it possible to query the database without sending the request to server? You need to make a request on change of first dropdown. Wiki suggestion is correct. POST or GET request is based on our requirement.

It might sound simple but there is significant work to be done here…

I am not sure of what context you have in terms of the relationship between the two dropdown list menus you are trying to connect, but I guess this is how I would do it (in retrospect reading back though this it’s no longer “how” - but actually this is what I did).

Assume that we have two database tables with associated Yii models, one for “Continents” in the world and one for “Countries” in the world, so we will relate to them as this:

Dropdown List 1: Continents
Dropdown List 2: Countries

Depending on which continent you choose from the first dropdown list, we then want this to determine and populate the list of options (countries) available in the second dropdown list - right?

This is what the model attributes look like:

Continents: id | name
Countries: id | name | continent_id

There are other columns contained within these models/database tables, but for this example we only care about those five. Each “country” belongs to a “continent” via the “continent_id” which relates directly to the “id” in the continents table/model.

You can see the relationship in these examples:

$continents = [
[‘id’ => ‘1’, ‘name’ => ‘Europe’],
[‘id’ => ‘2’, ‘name’ => ‘Africa’],
[‘id’ => ‘3’, ‘name’ => ‘Antartica’],
[‘id’ => ‘4’, ‘name’ => ‘Asia’],
[‘id’ => ‘5’, ‘name’ => ‘North America’],
[‘id’ => ‘6’, ‘name’ => ‘Australia’],
[‘id’ => ‘7’, ‘name’ => ‘South America’],
];

$countries = [
[‘id’ => ‘1’, ‘name’ => ‘Australia’, ‘continent_id’ => ‘6’],
[‘id’ => ‘2’, ‘name’ => ‘Belgium’, ‘continent_id’ => ‘1’],
[‘id’ => ‘3’, ‘name’ => ‘Canada’, ‘continent_id’ => ‘5’],
[‘id’ => ‘4’, ‘name’ => ‘China’, ‘continent_id’ => ‘4’],
[‘id’ => ‘5’, ‘name’ => ‘France’, ‘continent_id’ => ‘1’],
[‘id’ => ‘6’, ‘name’ => ‘Germany’, ‘continent_id’ => ‘1’],
[‘id’ => ‘7’, ‘name’ => ‘Great Britain’, ‘continent_id’ => ‘1’],
];

So firstly, we want to create the first dropdown list using the “id” and “name” from the continents model. You can either push in an array in the format of the example above, or you can push in a query from the continents model itself (I will post further below how I would do this).

In the code example below which I have written for you - you can see that the variable $continents is pushed into the dropdown list using the Yii ArrayHelper method via “id” and “name”. We then have some preferences for the dropdown list in the form of a helpful “prompt” (when nothing is selected), a style rule, and more importantly a javascript onchange behaviour which means that when the dropdown list is changed it will make an javascript Ajax request call.

This code will be placed in your view file, typically as part of a form:

<?= $form->field($model, 'continent_id')->dropDownList(
    ArrayHelper::map($continents, 'id', 'name'),
        [
            'prompt'=>'Please choose a continent',
            'style' => 'width: 100%;',
            'onchange' => '
                $.ajax({
                    type: "POST",
                    url: "'.Yii::$app->urlManager->createUrl(["site/dependent-dropdown-query"]).'",
                    data: {search_reference: $(this).val()},
                    dataType: "json",
                    success: function(response){

                        $("#countries-name").prop("disabled", false);
                        $("#countries-name").empty();
                        var count = response.length;

                        if(count === 0) {
                            $("#countries-name").empty();
                            $("#countries-name").prop("disabled", "disabled");
                            $("#countries-name").append("<option value=\'"+id+"\'>Sorry, there are no options available for this selection</option>");
                        } else {
                            $("#countries-name").append("<option value=\'"+id+"\'>Please choose a country...</option>");
                            for(var i = 0; i<count; i++){
                                var id = response[i][\'id\'];
                                var name = response[i][\'name\'];
                                $("#countries-name").append("<option value=\'"+id+"\'>"+name+"</option>");
                            }
                        }

                    }
                });
            '
        ]
    );
?>

So, as per above, the Ajax call will look for a url in your application, which of course needs to match the Yii url structure as “controller” / “action” (unless you have a module - then you would add it’s name at the front).

In my example I am calling the default “site” controller with an “action” called “dependent-dropdown-query”. It doesn’t need to go in the site controller, that’s just where I placed it as I was writing this for testing purposes. Anyhow, this is what that action looks like in my “site controller”:

This code will be placed inside your preferred controller, as a new action:

public function actionDependentDropdownQuery()
    {

        $search_reference = Yii::$app->request->post('search_reference');

        $query = new Query;
        $query->select('id, name')->from('countries')->where(['continent_id' => $search_reference]);
        $rows = $query->all();

        $data = [];
        if(!empty($rows)) {
            foreach($rows as $row) {
                $data[] = ['id' => $row['id'], 'name' => $row['name']];
            }
        } else {
            $data = '';
        }

        return $this->asJson($data);

    }

So, when the first dropdown list is changed, the Ajax call makes a request to the above controller/action, but as it does that it sends over the “id” value from the continent that has been selected in the first dropdown list as a variable called “search_reference”.

You can see this here:
data: {search_reference: $(this).val()},

Because it is a POST request we grab that and assign it to the $search_reference variable in our controller action which we then subsequently use to do our database query.

Once we have done our database query we assign the results to a new array called $data and push that back via the Ajax request in the format of Json (this is because we are talking to javascript here so we can’t send back a php array or some sort of php output, it must be a json object).

The response format we create is simply in the form of “id” and “name” so we can populate our second dropdown list. If there are no records found then we set $data to empty so we can catch that also if we need to in the response.

Now we place our second dropdownlist in our view file:

<?= $form->field($model, 'name')->dropDownList(
    ArrayHelper::map([], 'id', 'name'),
        [ 
            'disabled' => 'disabled',
            'prompt' => 'Please choose a continent first',
            'style' => 'width: 100%;',
        ],
    );
?>

The second dropdown doesn’t look like much does it? Basically it is expecting to receive an associative array of data in the form of “id” and “name” - which is exactly what we created in our controller action as per above.

And that’s pretty much it.

A few points to consider for your own purposes and some functionality which I have included:

  • In your controller file you may need to add access to this class so you can do your database query:
    use yii\db\Query;
  • You may also need to bring in your model classes, so for me it was the countries like so:
    use app\models\Countries;
  • The first dropdown list will be ready to use, but the second dropdown list will be disabled until you choose something from the first one - nice ux to have
  • The prompt will adjust depending on what state the second dropdown list is in
  • If there is no data found from your choice in the first dropdown then the second dropdown will understand this and disable itself again
  • For each form field dropdown you will need to allocate an appropriate $model, ‘name’ - typically based on the purpose of your form
  • In the Ajax request you will see references to and id “#countries-name”, this is the id of your second dropdown list so you will need to edit that as per your preference of whatever it is called
  • As for the dropdown list prompts and styling, you can adjust all that as per your preference also

I think that’s it, I will post up code for the database query for the first dropdown array - but it’s late for me now so I will do it tomorrow.

Hopefully this helps you and a few other people out because I see this from time-to-time and no one seems to post a proper answer - it always ends up as guess work.

You could use this same principle for many things like a dropdown list between a car “manufacturer” and their “models”, or whatever.

2 Likes

The answers below are helpful to me, so glad you asked this question. That said, have you looked at this packaged version that is ready to go? Dependent Dropdown - © Kartik

Thank you so much @replicant_k. I am truly grateful to you, for taking the time and effort on this. I will incorporate this and let you know how it goes.

Happy to report that I got this working! For some reason I had trouble with the select column with the array not getting populated with both id & name - but when I used select * it worked. But then I changed it to use my preferred method of ActiveDataProvider to process the rows. Thanks again - would not have been possible without your help and clear explanation. And I realize my original approach was incorrect and a POST is required on dropdown change.

This works great for initial insert, however when you are updating existing records, where the field values come from the database, there is an issue to be resolved. The dependent drop down is always disabled - easy to change, but the dependent drop down also has to be prepopulated with a dropdown values, corresponding to what value was retrieved from the parent dropdown.

I solved this by changing the view file as below:

<?= $form->field($model, 'name')->dropDownList(
    ArrayHelper::map($data, 'id', 'name'),
        [ 
            //'disabled' => 'disabled',
            'prompt' => 'Please choose a continent first',
            'style' => 'width: 100%;',
        ],
    );
?>

instead of
ArrayHelper::map([], 'id', 'name'),

where $data is defined as

$data = countrylookuptabel::find()
        ->where(['continent_name' => $model->continent])
        ->all();

In the initial insert $model->continent will be null/empty and the result will be a null/empty drop down which is what we need. On update, this will populate to the default list based on continent retrieved.

I have not solved the disable country dropdown on insert of if continent is null on update. I can live with the dependent dropdown always being enabled. Although this should not be too hard to make it dynamic.

Ah, ok - I don’t have the above connected to a live form so I never noticed that, but you would need to do something like this to handle if the form is actually being updated and the select lists have already been allocated values:

Option 1: Using vanilla Javascript which you can place at the bottom of your layout file:

<script>

    var selectListOne = document.getElementById("countries-continent_id"); 
    var selectListOneValue = selectListOne.options[selectListOne.selectedIndex].value;
    if(selectListOneValue.length > 0) {
        document.getElementById("countries-name").disabled = false;
    }

</script>

Option 2: Or, if you are using Jquery via a javascript file included in your AppAsset.php - or wherever you have it, then use this:

$(document).ready(function() {

    var selectListOneValue = $('#countries-continent_id').val();
    if(selectListOneValue.length > 0) {
        $('#countries-name').attr("disabled", false);
    } 

});

Basically we know that if there is no value in the drop down list then the id will be empty so we check if that returned value is more than nothing by counting the string length - that should work for both numbers and string values in the id (if anyone is doing that).

Remember and change the targetted id’s to whatever your dropdown lists are called - I used my example above i.e. ‘#countries-continent_id’ and ‘#countries-name

You can put this property back on again:

<?= $form->field($model, 'name')->dropDownList(
    ArrayHelper::map($data, 'id', 'name'),
        [ 
            'disabled' => 'disabled',
            'prompt' => 'Please choose a continent first',
            'style' => 'width: 100%;',
        ],
    );
?>

Let me know how it goes.

I was researching implementation and landed on window onload event - but I see document ready is probably a better event. With my lack (meaning zero) of javascript knowledge, I implemented it using PHP (as detailed above). I will come back to this later and test it out. Thanks again for updating - as always much appreciated.