Reader Stacks

How to Populate a Dropdown From an AJAX Response in jQuery

Clearing existing options before appending new ones is the detail that separates a working dependent-dropdown pattern from one that silently accumulates duplicates.

Populating a

Fetching and populating on change

$('#country').change(function () {
    const countryId = $(this).val();

    $.get('/api/states', { country_id: countryId }, function (states) {
        $('#state').empty();
        $('#state').append('');

        $.each(states, function (index, state) {
            $('#state').append(``);
        });
    });
});

$('#state').empty() before appending new options is the essential step — skipping it means every subsequent country selection appends more options on top of the previous ones, silently accumulating duplicate entries in the dropdown across multiple selections.

The Laravel endpoint returning the JSON data

Route::get('/api/states', function (Request $request) {
    return State::where('country_id', $request->country_id)
        ->select('id', 'name')
        ->orderBy('name')
        ->get();
});

Showing a loading state while the request is in flight

$('#country').change(function () {
    const countryId = $(this).val();
    $('#state').prop('disabled', true).empty().append('');

    $.get('/api/states', { country_id: countryId }, function (states) {
        $('#state').prop('disabled', false).empty();
        $('#state').append('');

        $.each(states, function (index, state) {
            $('#state').append(``);
        });
    });
});

Disabling the dropdown and showing a temporary "Loading..." option prevents the user from interacting with a dropdown that doesn't yet reflect the newly selected country's actual states.

Pre-selecting a value, for an edit form

$.get('/api/states', { country_id: countryId }, function (states) {
    $('#state').empty();

    $.each(states, function (index, state) {
        const selected = state.id === existingStateId ? 'selected' : '';
        $('#state').append(``);
    });
});

For an edit form pre-populating an existing record's saved state, comparing each option's ID against the previously saved value and adding the selected attribute is what restores the correct existing selection once the dependent dropdown's options load in.

Escaping dynamic content to avoid an XSS risk

function escapeHtml(text) {
    return $('
').text(text).html(); } $('#state').append(``);

If the option text ever originates from user-editable data (rather than a fixed, trusted list like countries/states), escaping it before inserting into the DOM avoids an XSS vulnerability from a maliciously crafted name value — using jQuery's own .text() method to escape, as shown, is a simple, reliable approach.