Populating a dropdown with options fetched via AJAX — often a "state" dropdown populated based on a chosen "country," a classic dependent-dropdown pattern — is a matter of clearing existing options and appending new ones built from the response data.
The dependent dropdown markup
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.