2
\$\begingroup\$

I have written the following JS to populate dependent (aka cascading) select boxes for country, city and state selection.

The backend is a Rails application, which provides the following:

  • Object literal where each element consists of a state ISO codes and their corresponding names at /states/countryIsoCode
  • Array of city names at `/cities/countryIsoCode/stateIsoCode/

The following code is used when creating new users and updating existing users. Please keep in mind that when an existing user is updated, he may change only the city, only state and city or all three values.

The HTML consists of three plain select boxes.

  • When creating a new user: The country select box is created with a list of countries and the other two are created without any options, as their options are populated by the JS code below.
  • When updating an existing user: The server populates the country box with all countries and selects the user's country. The state box is populated with that country's states and the user's state is selected. Same with the city box.

JS is not my primary language, so I appreciate your help to improve the code readability, maintainability, efficiency, reduce repetition and so on.

"use strict";
/*jshint esversion: 6 */

var prepareDropdowns = function(dropdownSelectors) {
    dropdownSelectors.empty();
    // We need the first option to be blank for the following reasons:
    // 1. When updating the country of an existing user, if the country has
    // no states or cities (e.g., Vatican), a blank value must be sent to
    // the server to remove the user's previous state and city.
    // 2. When a dropdown is populated, the browser automatically selects the
    // first option. If the user tries to select the first option from the
    // "state" dropdown,it does not trigger the 'change' event required to
    // populate the "city" dropdown.
    dropdownSelectors.prepend("<option value='' selected='selected'></option>");
    // Disable it until options are populated
    dropdownSelectors.prop("disabled", true);
}

var getStateList = function() {
    var countryIsoCode = $("#user_country").val();
    $.ajax({
        dataType: "json",
        url: `/states/${countryIsoCode}`,
        // The first argument passed to the callback function is the JSON
        // data, consisting of an object where each property is a state code
        // and its value is the corresponding state name.
        success: addOptionsToStateDropdown,
        // jqXHR, textStatus, errorThrown are passed as args to the callback
        error: ZenUtils.handleAjaxError
    });
};

var getCityList = function() {
    var countryIsoCode = $("#user_country").val();
    var stateIsoCode = $("#user_state").val();
    $.ajax({
        dataType: "json",
        url: `/cities/${countryIsoCode}/${stateIsoCode}`,
        // The first argument passed to the callback function is the JSON
        // data, consisting of an array of city names.
        success: addOptionsToCityDropdown,
        // jqXHR, textStatus, errorThrown are passed as args to the callback
        error: ZenUtils.handleAjaxError
    });
};


var addOptionsToStateDropdown = function(stateList) {
    // Some countries have no states
    if (!ZenUtils.isEmpty(stateList)) {
        for (var StateIsoCode in stateList) {
            $("#user_state")
                .append(`<option value="${StateIsoCode}">${stateList[StateIsoCode]}</option>`);
        }
        $("#user_state").removeProp("disabled");
    }
}

// Receives an array of city names as an argument
var addOptionsToCityDropdown = function(cityList) {
    // Some countries have no states and cities
    if (!ZenUtils.isEmpty(cityList)) {
        for (var cityName of cityList) {
            $("#user_city").append(`<option value="${cityName}">${cityName}</option>`);
        }
        $("#user_city").removeProp("disabled");
    }
};


var populateStateDropdown = function() {
    prepareDropdowns($("#user_state, #user_city"))
    getStateList();
};

var populateCityDropdown = function() {
    prepareDropdowns($("#user_city"))
    getCityList();
};

var enableDropdowns = function() {
    $("#user_state").removeProp("disabled");
    $("#user_city").removeProp("disabled");
};

$(document).on("turbolinks:load", function() {
    $("#user_country").on("change", populateStateDropdown);
    $("#user_state").on("change", populateCityDropdown);
    // The state and city dropdowns are be disabled if the selected country has
    // no states or cities. They must be reenabled before submitting the form in
    // order to send blank values instead of not sending anything (key is
    // absent from the params hash). If a blank value is not sent, when changing
    // the country for an existing user, the old city and state will remain in
    // the model and cause a validation error (invalid country/city/state
    // combination).
    $("form").on("submit", enableDropdowns);
});
\$\endgroup\$
  • 1
    \$\begingroup\$ I am thinking how more readable this would be in react/redux but I don't think you can improve the code all that much with jquery. The biggest problem is the combination of UI, requests and model in one place, which makes it hard to read and understand. \$\endgroup\$ – Sulthan Aug 3 '17 at 18:09
  • \$\begingroup\$ @Sulthan: I haven't yet used React, Angular or anything similar. But I can imagine how applying that kind of separation of concerns must make the code a lot more readable; probably in the same way that Rails apps benefit greatly from splitting their code amongst MVC layers. I'll probably learn React or Angular soon. \$\endgroup\$ – BrunoFacca Aug 3 '17 at 18:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.