Dynamic Dependent AJAX Dropdowns – Laravel 12

In this comprehensive tutorial, I’ll guide you through building an interactive dependent dropdown system in Laravel 12. We’ll implement a cascading selection menu for countries, states, and cities that dynamically updates based on user selections.

Understanding Dependent Dropdowns

A dependent dropdown is an intelligent form element where the options in one dropdown menu change based on the selection made in a previous dropdown. For instance:

  • Selecting “United States” in the country dropdown would populate the state dropdown with options like “California” and “New York”
  • Choosing “California” would then populate the city dropdown with options like “Los Angeles” and “San Francisco”

This creates a seamless, intuitive user experience by only showing relevant options at each level.

Implementation Overview

We’ll follow these steps to create our dynamic dropdown system:

  1. Set up a fresh Laravel 12 application
  2. Create database tables for countries, states, and cities
  3. Establish model relationships
  4. Implement controller logic
  5. Build the frontend with AJAX functionality
  6. Seed sample data
  7. Test our implementation

Step-by-Step Implementation

Step 1: Install Laravel 12

Begin by creating a new Laravel project:

composer create-project laravel/laravel dependent-dropdown-demo
cd dependent-dropdown-demo

Step 2: Create Database Migrations

Generate and configure migrations for our three tables:

php artisan make:migration create_countries_states_cities_tables

Update the migration file:

<?php
  
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
  
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('countries', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
  
        Schema::create('states', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->foreignId('country_id')->constrained();
            $table->timestamps();
        });
  
        Schema::create('cities', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->foreignId('state_id')->constrained(); 
            $table->timestamps();
        });
    }
  
    public function down(): void
    {
        Schema::dropIfExists('cities');
        Schema::dropIfExists('states');
        Schema::dropIfExists('countries');
    }
};

Run the migrations:

php artisan migrate

Step 3: Create Eloquent Models

Generate models with their relationships:

php artisan make:model Country
php artisan make:model State
php artisan make:model City

Update the model files:

Country.php

<?php
  
namespace App\Models;
  
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
  
class Country extends Model
{
    use HasFactory;
  
    protected $fillable = ['name'];
    
    public function states(): HasMany
    {
        return $this->hasMany(State::class);
    }
}

State.php

<?php
  
namespace App\Models;
  
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
  
class State extends Model
{
    use HasFactory;
  
    protected $fillable = ['name', 'country_id'];
    
    public function country(): BelongsTo
    {
        return $this->belongsTo(Country::class);
    }
    
    public function cities(): HasMany
    {
        return $this->hasMany(City::class);
    }
}

City.php

<?php
  
namespace App\Models;
  
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
  
class City extends Model
{
    use HasFactory;
  
    protected $fillable = ['name', 'state_id'];
    
    public function state(): BelongsTo
    {
        return $this->belongsTo(State::class);
    }
}

Step 4: Define Routes

Set up the necessary routes in routes/web.php:

<?php
  
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\DropdownController;
    
Route::get('dropdown', [DropdownController::class, 'index']);
Route::post('api/fetch-states', [DropdownController::class, 'fetchState']);
Route::post('api/fetch-cities', [DropdownController::class, 'fetchCity']);

Step 5: Create the Controller

Generate and implement the controller logic:

php artisan make:controller DropdownController

Update the controller:

<?php
  
namespace App\Http\Controllers;
  
use Illuminate\Http\Request;
use App\Models\Country;
use App\Models\State;
use App\Models\City;
use Illuminate\View\View;
use Illuminate\Http\JsonResponse;
  
class DropdownController extends Controller
{
    public function index(): View
    {
        return view('dropdown', [
            'countries' => Country::all(['name', 'id'])
        ]);
    }

    public function fetchState(Request $request): JsonResponse
    {
        $states = State::where('country_id', $request->country_id)
                      ->get(['name', 'id']);
  
        return response()->json(['states' => $states]);
    }

    public function fetchCity(Request $request): JsonResponse
    {
        $cities = City::where('state_id', $request->state_id)
                      ->get(['name', 'id']);
                                      
        return response()->json(['cities' => $cities]);
    }
}

Step 6: Create the View

Build the frontend interface in resources/views/dropdown.blade.php:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Laravel 12 Dependent Dropdown Demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <div class="container py-5">
        <div class="card shadow">
            <div class="card-header bg-primary text-white">
                <h3 class="mb-0">Dynamic Dependent Dropdown Demo</h3>
            </div>
            <div class="card-body">
                <form>
                    <div class="mb-3">
                        <label for="country-dropdown" class="form-label">Country</label>
                        <select id="country-dropdown" class="form-select">
                            <option value="">-- Select Country --</option>
                            @foreach ($countries as $country)
                                <option value="{{ $country->id }}">{{ $country->name }}</option>
                            @endforeach
                        </select>
                    </div>
                    
                    <div class="mb-3">
                        <label for="state-dropdown" class="form-label">State</label>
                        <select id="state-dropdown" class="form-select" disabled>
                            <option value="">-- Select State --</option>
                        </select>
                    </div>
                    
                    <div class="mb-3">
                        <label for="city-dropdown" class="form-label">City</label>
                        <select id="city-dropdown" class="form-select" disabled>
                            <option value="">-- Select City --</option>
                        </select>
                    </div>
                </form>
            </div>
        </div>
    </div>

    <script>
        $(document).ready(function() {
            // Country dropdown change event
            $('#country-dropdown').change(function() {
                const countryId = $(this).val();
                $('#state-dropdown').html('<option value="">-- Select State --</option>');
                $('#state-dropdown').prop('disabled', !countryId);
                
                if (countryId) {
                    $.ajax({
                        url: "{{ url('api/fetch-states') }}",
                        type: "POST",
                        data: {
                            country_id: countryId,
                            _token: '{{ csrf_token() }}'
                        },
                        success: function(response) {
                            $.each(response.states, function(key, value) {
                                $('#state-dropdown').append(
                                    `<option value="${value.id}">${value.name}</option>`
                                );
                            });
                        }
                    });
                }
                
                // Reset city dropdown
                $('#city-dropdown').html('<option value="">-- Select City --</option>');
                $('#city-dropdown').prop('disabled', true);
            });

            // State dropdown change event
            $('#state-dropdown').change(function() {
                const stateId = $(this).val();
                $('#city-dropdown').html('<option value="">-- Select City --</option>');
                $('#city-dropdown').prop('disabled', !stateId);
                
                if (stateId) {
                    $.ajax({
                        url: "{{ url('api/fetch-cities') }}",
                        type: "POST",
                        data: {
                            state_id: stateId,
                            _token: '{{ csrf_token() }}'
                        },
                        success: function(response) {
                            $.each(response.cities, function(key, value) {
                                $('#city-dropdown').append(
                                    `<option value="${value.id}">${value.name}</option>`
                                );
                            });
                        }
                    });
                }
            });
        });
    </script>
</body>
</html>

Step 7: Seed Sample Data

Create and run a seeder to populate our tables:

php artisan make:seeder CountryStateCitySeeder

Update the seeder:

<?php
  
namespace Database\Seeders;
  
use Illuminate\Database\Seeder;
use App\Models\Country;
use App\Models\State;
use App\Models\City;
  
class CountryStateCitySeeder extends Seeder
{
    public function run(): void
    {
        // United States data
        $us = Country::create(['name' => 'United States']);
        
        $florida = State::create(['country_id' => $us->id, 'name' => 'Florida']);
        City::create(['state_id' => $florida->id, 'name' => 'Miami']);
        City::create(['state_id' => $florida->id, 'name' => 'Tampa']);
        
        $california = State::create(['country_id' => $us->id, 'name' => 'California']);
        City::create(['state_id' => $california->id, 'name' => 'Los Angeles']);
        City::create(['state_id' => $california->id, 'name' => 'San Francisco']);
        
        // India data
        $india = Country::create(['name' => 'India']);
        
        $gujarat = State::create(['country_id' => $india->id, 'name' => 'Gujarat']);
        City::create(['state_id' => $gujarat->id, 'name' => 'Ahmedabad']);
        City::create(['state_id' => $gujarat->id, 'name' => 'Surat']);
        
        $maharashtra = State::create(['country_id' => $india->id, 'name' => 'Maharashtra']);
        City::create(['state_id' => $maharashtra->id, 'name' => 'Mumbai']);
        City::create(['state_id' => $maharashtra->id, 'name' => 'Pune']);
    }
}

Run the seeder:

php artisan db:seed --class=CountryStateCitySeeder

Step 8: Launch the Application

Start the development server:

php artisan serve

Visit the application in your browser at http://localhost:8000/dropdown to see the dynamic dropdowns in action.

Previous Article

Laravel 12 : Implementing Google Maps Autocomplete Address

Next Article

Store JSON In Database - Laravel 12 - A Complete Guide

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *


Subscribe to our Newsletter

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨