Dynamic Dependent Dropdowns: CodeIgniter 4 with AJAX Guide

In web applications, auto populating dropdowns are commonly used to simplify user input and ensure data consistency. When a user selects a value from one dropdown, another dropdown with related data is dynamically populated.

Consider this example. Assume we have a global online store and want to allow our customers to filter products by country and city. There could be two dropdowns: one for countries and one for cities. The city dropdown will dynamically change depending on the country selected by the user, displaying only cities in that country. This feature will make it easier for users to select the correct filter, reducing confusion and providing a better user experience.

In this tutorial, I show how you can create a dynamic dependent dropdown with MySQL database data using jQuery AJAX in CodeIgniter 4.

Dynamic Dependent Dropdowns: CodeIgniter 4 with AJAX Guide


Table of Content

  1. Database configuration
  2. Enable CSRF
  3. Create Tables using Migration
  4. Create Models
  5. Create Routes
  6. Create Controller
  7. Create View
  8. Demo
  9. Conclusion

1. Database configuration

  • Open .env file which is available at the project root.

NOTE – If dot (.) not added at the start then rename the file to .env.

  • Remove # from start of database.default.hostname, database.default.database, database.default.username, database.default.password, database.default.DBDriver, database.default.DBPrefix, and database.default.port.
  • Update the configuration and save it.
database.default.hostname = 127.0.0.1
database.default.database = codeigniterdb
database.default.username = root
database.default.password = root
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306

2. Enable CSRF

  • Again open .env file.
  • Remove # from the start of the security.tokenName,security.headerName, security.cookieName, security.expires,and security.regenerate.
  • I update the security.tokenName value with 'csrf_hash_name'. With this name read CSRF hash. You can update it with any other value.
  • If you don’t want to regenerate CSRF hash after each request then set security.regenerate = false.
security.tokenName = 'csrf_hash_name' 
security.headerName = 'X-CSRF-TOKEN' 
security.cookieName = 'csrf_cookie_name' 
security.expires = 7200 
security.regenerate = true
  • Open app/Config/Filters.php file.
  • Uncomment 'csrf' in 'before' if commented.
// Always applied before every request
public $globals = [
    'before' => [
       // 'honeypot',
       'csrf',
       // 'invalidchars',
    ],
    'after' => [
       'toolbar',
       // 'honeypot',
       // 'secureheaders',
    ],
];

3. Create Tables using Migration

Create 3 new tables using migration –

  • countries – Store countries name.
  • states – Store country states name.
  • cities – Store state cities name.

countries Table –

  • Create countries table.
php spark migrate:create create_countries_table
  • Now, navigate to app/Database/Migrations/ folder from the project root.
  • Find a PHP file that ends with CreateCountriesTable open it.
  • Define the table structure in the up() method.
  • Using the down() method delete countries table that calls when undoing migration.
<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreateCountriesTable extends Migration
{
     public function up(){
          $this->forge->addField([
                'id' => [
                      'type' => 'INT',
                      'constraint' => 10,
                      'unsigned' => true,
                      'auto_increment' => true,
                ],
                'name' => [
                      'type' => 'VARCHAR',
                      'constraint' => '60',
                ]
          ]); 
          $this->forge->addKey('id', true); 
          $this->forge->createTable('countries'); 
     }

     public function down(){
          $this->forge->dropTable('countries'); 
     }
}

states Table –

  • Create states table.
php spark migrate:create create_states_table
  • Navigate to app/Database/Migrations/ folder and find PHP file that ends with CreateStatesTable.
  • Define table structure in up() method.
  • Here, I added a foreign key to country_id field.
  • Using the down() method delete states table that calls when undoing migration.
<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreateStatesTable extends Migration
{
      public function up(){
           $this->forge->addField([
                 'id' => [
                      'type' => 'INT',
                      'constraint' => 10,
                      'unsigned' => true,
                      'auto_increment' => true,
                 ],
                 'country_id' => [
                      'type' => 'INT',
                      'constraint' => 10,
                      'unsigned' => true,
                 ],
                 'name' => [
                      'type' => 'VARCHAR',
                      'constraint' => '60',
                 ]
           ]); 
           $this->forge->addKey('id', true);
           $this->forge->addForeignKey('country_id', 'countries', 'id', 'CASCADE', 'CASCADE');
           $this->forge->createTable('states');

      }

      public function down(){
           $this->forge->dropTable('states'); 
      }
}

cities Table –

  • Create cities table.
php spark migrate:create create_cities_table
  • Navigate to app/Database/Migrations/ folder and find PHP file that ends with CreateCitiesTable.
  • Define table structure in up() method.
  • Here, I added a foreign key to state_id field.
  • Using the down() method delete cities table that calls when undoing migration.
<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreateCitiesTable extends Migration
{
      public function up(){
            $this->forge->addField([
                 'id' => [
                      'type' => 'INT',
                      'constraint' => 10,
                      'unsigned' => true,
                      'auto_increment' => true,
                 ],
                 'state_id' => [
                      'type' => 'INT',
                      'constraint' => 10,
                      'unsigned' => true,
                 ],
                 'name' => [
                      'type' => 'VARCHAR',
                      'constraint' => '60',
                 ]
            ]); 
            $this->forge->addKey('id', true);
            $this->forge->addForeignKey('state_id', 'states', 'id', 'CASCADE', 'CASCADE');
            $this->forge->createTable('cities');
      }

      public function down(){
            $this->forge->dropTable('cities'); 
      }
}
  • Run the migration –
php spark migrate

I added some records to the table.


4. Create Models

Create 3 Models –

  • Countries
  • States
  • Cities

Countries Model –

  • Create Countries Model.
php spark make:model Countries
  • Open app/Models/Countries.php file.
  • In $allowedFields Array specify field names – ['name'] that can be set during insert and update.

Completed Code

<?php

namespace App\Models;

use CodeIgniter\Model;

class Countries extends Model
{
      protected $DBGroup = 'default';
      protected $table = 'countries';
      protected $primaryKey = 'id';
      protected $useAutoIncrement = true;
      protected $insertID = 0;
      protected $returnType = 'array';
      protected $useSoftDeletes = false;
      protected $protectFields = true;
      protected $allowedFields = ['name'];

      // Dates
      protected $useTimestamps = false;
      protected $dateFormat = 'datetime';
      protected $createdField = 'created_at';
      protected $updatedField = 'updated_at';
      protected $deletedField = 'deleted_at';

      // Validation
      protected $validationRules = [];
      protected $validationMessages = [];
      protected $skipValidation = false;
      protected $cleanValidationRules = true;

      // Callbacks
      protected $allowCallbacks = true;
      protected $beforeInsert = [];
      protected $afterInsert = [];
      protected $beforeUpdate = [];
      protected $afterUpdate = [];
      protected $beforeFind = [];
      protected $afterFind = [];
      protected $beforeDelete = [];
      protected $afterDelete = [];
}

States Model –

  • Create States Model.
php spark make:model States
  • Open app/Models/States.php file.
  • In $allowedFields Array specify field names – ['country_id','name'].

Completed Code

<?php

namespace App\Models;

use CodeIgniter\Model;

class States extends Model
{
      protected $DBGroup = 'default';
      protected $table = 'states';
      protected $primaryKey = 'id';
      protected $useAutoIncrement = true;
      protected $insertID = 0;
      protected $returnType = 'array';
      protected $useSoftDeletes = false;
      protected $protectFields = true;
      protected $allowedFields = ['country_id','name'];

      // Dates
      protected $useTimestamps = false;
      protected $dateFormat = 'datetime';
      protected $createdField = 'created_at';
      protected $updatedField = 'updated_at';
      protected $deletedField = 'deleted_at';

      // Validation
      protected $validationRules = [];
      protected $validationMessages = [];
      protected $skipValidation = false;
      protected $cleanValidationRules = true;

      // Callbacks
      protected $allowCallbacks = true;
      protected $beforeInsert = [];
      protected $afterInsert = [];
      protected $beforeUpdate = [];
      protected $afterUpdate = [];
      protected $beforeFind = [];
      protected $afterFind = [];
      protected $beforeDelete = [];
      protected $afterDelete = [];
}

Cities Model –

  • Create Cities Model.
php spark make:model Cities
  • Open app/Models/Cities.php file.
  • In $allowedFields Array specify field names – ['state_id','name'].

Completed Code

<?php

namespace App\Models;

use CodeIgniter\Model;

class Cities extends Model
{
      protected $DBGroup = 'default';
      protected $table = 'cities';
      protected $primaryKey = 'id';
      protected $useAutoIncrement = true;
      protected $insertID = 0;
      protected $returnType = 'array';
      protected $useSoftDeletes = false;
      protected $protectFields = true;
      protected $allowedFields = ['state_id','name'];

      // Dates
      protected $useTimestamps = false;
      protected $dateFormat = 'datetime';
      protected $createdField = 'created_at';
      protected $updatedField = 'updated_at';
      protected $deletedField = 'deleted_at';

      // Validation
      protected $validationRules = [];
      protected $validationMessages = [];
      protected $skipValidation = false;
      protected $cleanValidationRules = true;

      // Callbacks
      protected $allowCallbacks = true;
      protected $beforeInsert = [];
      protected $afterInsert = [];
      protected $beforeUpdate = [];
      protected $afterUpdate = [];
      protected $beforeFind = [];
      protected $afterFind = [];
      protected $beforeDelete = [];
      protected $afterDelete = [];
}

5. Create Routes

  • Open app/Config/Routes.php file.
  • Define 3 routes –
    • /
    • page/fetchCountryStates– Fetch all states of the selected country.
    • page/fetchStateCities – Fetch all cities of the selected state.
$routes->get('/', 'PagesController::index');
$routes->post('page/fetchCountryStates', 'PagesController::fetchCountryStates');
$routes->post('page/fetchStateCities', 'PagesController::fetchStateCities');

6. Create Controller

  • Create PagesController Controller –
php spark make:controller PagesController
  • Include Countries, States, and Cities Models.

Create 3 methods –

  • index() – Fetch all records from the countries table and assign it to $data['countries']. Load index view and pass $data Array.
  • fetchCountryStates() – This method is use to handle AJAX requests and return states list by country_id.

Read POST country_id value and assign it to $country_id variable. Fetch records from the states table where country_id = $country_id. Assign fetched records to $response['states'] Array.

In the $response Array also store a new CSRF hash to the token key.

Return $response Array in JSON format.

  • fetchStateCities() – This method is also use to handle AJAX requests and return cities list by state_id.

Read POST state_id value and assign it to $state_id variable. Fetch records from the cities table where state_id = $state_id. Assign fetched records to $response['cities']. Store new CSRF hash to $response['token'].

Return $response Array in JSON format.

Completed Code

<?php

namespace App\Controllers;

use App\Controllers\BaseController;
use App\Models\Countries;
use App\Models\States;
use App\Models\Cities;

class PagesController extends BaseController
{
      public function index(){

           ## Fetch all Countries
           $countryObj = new Countries();
           $countriesList = $countryObj->select('*')->findAll();

           $data['countries'] = $countriesList;

           return view('index',$data); 
      }

      // Return country states list
      public function fetchCountryStates(){

           ## Read POST data
           $request = service('request');
           $postData = $request->getPost();

           $country_id = $postData['country_id'];

           ## Fetch states list by country_id
           $stateObj = new States();
           $statesList = $stateObj->select('id,name')
                   ->where('country_id',$country_id)
                   ->find();

           $response['states'] = $statesList;

           ## New CSRF token
           $response['token'] = csrf_hash();

           return $this->response->setJSON($response);

      }

      // Return state cities list
      public function fetchStateCities(){

           ## Read POST data
           $request = service('request');
           $postData = $request->getPost();

           $state_id = $postData['state_id'];

           ## Fetch cities list by state_id
           $cityObj = new Cities();
           $citiesList = $cityObj->select('id,name')
                  ->where('state_id',$state_id)
                  ->find();

           $response['cities'] = $citiesList;

           ## New CSRF token
           $response['token'] = csrf_hash();

           return $this->response->setJSON($response);
      }
}

7. Create View

  • Create index.php file in app/Views/ folder.

HTML

  • Create a hidden field to store CSRF hash in value attribute and CSRF name in the name attribute.
  • Create 3 <select > elements –
    • <select id="sel_country" > is for country selection. Loop on $countries Array to add country <option >.
    • Empty <select id="sel_state" > element for state selection.
    • Empty <select id="sel_city" > element for city selection.

jQuery Script

Country selection changed (Load country states) –

  • Define change event on <select id="sel_country" >.
  • Empty the <select id="sel_state"> and <select id="sel_city"> elements, read the selected value, and send an AJAX POST request to <?=site_url('page/fetchCountryStates')?> if it is not empty. Set dataType to json and pass the CSRF token and country id as data.
  • On successful callback update CSRF token in hidden element with response.token.
  • Loop on the states list and add new <option > to <select id="sel_state">.

State selection changed (Load state cities) – 

  • Define change event on <select id="sel_state" >.
  • Empty the <select id="sel_city">, read the selected value, and send an AJAX POST request to <?=site_url('page/fetchStateCities')?> if it is not empty. Set dataType to json and pass the CSRF token and state id as data.
  • On successful callback update CSRF token in hidden element with response.token.
  • Loop on the citites list and add new <option > to <select id="sel_city">.

Completed Code

<!DOCTYPE html>
<html>
<head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>How to Create Dynamic Dependent Dropdowns with AJAX in CodeIgniter 4</title>
  
     <style type="text/css">
     body{
          font-size: 18px;
     }
     select{
          width: 200px;
          padding: 5px;
          font-size: 18px;
     }
     </style>

</head>
<body>

     <!-- CSRF token --> 
     <input type="hidden" class="txt_csrfname" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" />

     Country: <br>
     <select id="sel_country" class="form-control">
          <option value="">-- Select Country --</option>
          <?php 
          foreach($countries as $country){
          ?>
                <option value="<?= $country['id'] ?>"><?= $country['name'] ?></option>
          <?php
          }
          ?>
     </select>

     <br><br>

     State: <br>
     <select id="sel_state" class="form-control">
          <option value="">-- Select State --</option> 
     </select>

     <br><br>

     City: <br>
     <select id="sel_city" class="form-control">
          <option value="">-- Select City --</option> 
     </select>

     <!-- Script -->
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
     <script type="text/javascript">
     $(document).ready(function(){

          // Country selection change
          $('#sel_country').change(function(){

               var country_id = $(this).val();

               // Empty state and city dropdown
               $('#sel_state').find('option').not(':first').remove();
               $('#sel_city').find('option').not(':first').remove();

               if(country_id != ''){

                    // CSRF Hash
                    var csrfName = $('.txt_csrfname').attr('name'); // CSRF Token name
                    var csrfHash = $('.txt_csrfname').val(); // CSRF hash

                    // AJAX request
                    $.ajax({
                         url:"<?=site_url('page/fetchCountryStates')?>",
                         type: 'post',
                         data: {[csrfName]: csrfHash,country_id: country_id},
                         dataType: 'json',
                         success: function(response){

                               // Update CSRF Token
                               $('.txt_csrfname').val(response.token);

                               // Read state list
                               var len = response.states.length;

                               // Add data to state dropdown
                               for( var i = 0; i<len; i++){
                                     var id = response.states[i]['id'];
                                     var name = response.states[i]['name'];

                                     $("#sel_state").append("<option value='"+id+"'>"+name+"</option>");
                               }

                         }
                    });
               }
          });

          // State selection change
          $('#sel_state').change(function(){

               var state_id = $(this).val();

               // Empty city dropdown
               $('#sel_city').find('option').not(':first').remove();

               if(state_id != ''){

                      // CSRF Hash
                      var csrfName = $('.txt_csrfname').attr('name'); // CSRF Token name
                      var csrfHash = $('.txt_csrfname').val(); // CSRF hash

                      // AJAX request
                      $.ajax({
                           url:"<?=site_url('page/fetchStateCities')?>",
                           type: 'post',
                           data: {[csrfName]: csrfHash,state_id: state_id},
                           dataType: 'json',
                           success: function(response){

                                 // Update CSRF Token
                                 $('.txt_csrfname').val(response.token);

                                 // Read city list
                                 var len = response.cities.length;

                                 // Add data to city dropdown
                                 for( var i = 0; i<len; i++){
                                       var id = response.cities[i]['id'];
                                       var name = response.cities[i]['name'];

                                       $("#sel_city").append("<option value='"+id+"'>"+name+"</option>");

                                 }

                           }
                      });
               }
          });

     });
     </script>

</body>
</html>

8. Demo

View Demo


9. Conclusion

Auto populating dropdowns using AJAX is a valuable feature that improves the user experience in web applications and reduces the risk of incorrect data input. By using AJAX, you can dynamically update the contents of a dropdown.

By following the tutorial, you can easily create multiple dynamic dependent dropdowns on your page.

Remove the CSRF token code if it is not enabled in your project.

If data is not loading in the dropdown after selection then debug it using the browser console and network tab, and recheck the code and SQL queries.

View more CodeIgniter 4 tutorials.

If you found this tutorial helpful then don't forget to share.

Leave a Comment