0

I have a pair of select boxes where the user must first select the country, then select a city or region in that country:

<select name="ListingCountry" id="ListingCountry">
 <option value="0">-- Choose the country --</option>
 <option value="1" selected="selected">United Kingdom</option>
</select>
<select name="ListingCity" id="ListingCity" class="city">
 <option value="0">-- Choose the city or region --</option>
</select>

The 2nd <select> needs to update dynamically based on the selection of the 1st - i.e. only UK cities and regions shown when UK is selected.

The lists are generated from a database and are set into an array:

var cityOptions =
{
 1 :
 [
  {
   37: "London",
   1: "Bedfordshire",
   2: "Berkshire",
   3: "Birmingham",
   ...
  }
 ]
};

1 towards the top references the country ID.

The ID numbers are auto-generated by the database. I want the capital to always be at the top of the list, and achieve this in the array print by using a CityOrder field and sorting by that CityOrder descending - hence why London (with a CityOrder of 1) is at the top of the list despite having a higher ID than others which all have a CityOrder of 0.

I then use jQuery to dynamically update the lists:

function updateCityOptions(currentCity)
{
 var $countryAll = $("#ListingCity");
 var $countrySel = $("#ListingCountry option:selected").val();
 if($countrySel!=0)
 {
  $('#ListingCity option:gt(0)').remove();
  $.each(cityOptions[$countrySel], function(k,v)
  {
   $.each(cityOptions[$countrySel][k], function(k2,v2)
   {
    if(k2==currentCity)
    {
     $countryAll.append($('<option selected="selected"></option>').attr("value", k2).text(v2));
    }
    else
    {
     $countryAll.append($('<option></option>').attr("value", k2).text(v2));
    }
   });
  });
  $(".city").fadeIn(300);
 }
 else
 {
  $('#ListingCity option:gt(0)').remove();
  $(".city").fadeOut(300);
 }
}

The problem is that the order of the select box when a country is chosen seems to be in numerical order of the array, not the order in which it's printed in the DOM - i.e. Bedfordshire appears first in the list (ID number 1) and London gets lost somewhere in the middle.

Any clues?

Stuart Pinfold
  • 313
  • 1
  • 5
  • 18
  • Is London the `currentCity` in the function argument? You can just use `.prepend` instead of `.append` to insert it at the beginning of the ` – litel Apr 18 '16 at 19:16
  • @StuartPinfold, could you give some feed-back or accept the answer that most suits you? – trincot May 02 '16 at 08:41

2 Answers2

1

Object properties are not stored in the order you expect. When you iterate the object properties -- like with $.each, which performs a for ... in on objects -- the order will depend on a few factors. First of all, it depends on whether you are running on pre-ES6 JavaScript: then there is no guarantee of any determined order.

ES6 has brought a change in this, see "Sort JavaScript Object by Key", and when object properties are numerical, they will be iterated over in numerical order.

Still, for all lists where order is important, I would advise to use arrays. Instead of this:

{
   37: "London",
   1: "Bedfordshire",
   2: "Berkshire",
   3: "Birmingham",
   ...
}

Produce and use something like this:

[
   { id: 37, name: "London"},
   { id: 1, name: "Bedfordshire"},
   { id: 2, name: "Berkshire"},
   { id: 3, name: "Birmingham"},
   ...
]

... and adapt your code to that structure.

Community
  • 1
  • 1
trincot
  • 211,288
  • 25
  • 175
  • 211
0

I looked this over a bit and at first thought it should be similar to other postings however you do not sort by either the ID or the Name (text) but by some other value. In order to facilitate that, I created an object that holds all the countries and cities and then puts them in. I then sort using a sortOrder from that that gets pushed into the options list as data-sort and a function to do the sort.

I see your currentCity but that would likely change on a country change? So I put a "default" for each country by putting an isDefault:true on a city.

I also pushed the countries in from the list as well, with a default there.

This may be overkill for your situation but take what you like from this.

Create a namespace

var myApp = myApp || {};// create a namespace to use. 

Add a countries object:

myApp.countries = [{
  "name": "United Kingdom",
  isDefault: true,
  id: 1,
  "cities": [{
    id: 1,
    name: "Bedfordshire",
    "sortOrder": 1,
    "isCapital": false
  }, {
    id: 2,
    name: "Berkshire",
    "sortOrder": 2,
    "isCapital": false
  }, {
    id: 3,
    name: "Birmingham",
    "sortOrder": 3,
    "isCapital": false
  }, {
    id: 4,
    name: "Brighton",
    "sortOrder": 4,
    "isCapital": false,
     isDefault:true
  }, {
    id: 5,
    name: "Buckinghamshire",
    "sortOrder": 6,
    "isCapital": false
  }, {
    id: 6,
    name: "Appleton",
    "sortOrder": 5,
    "isCapital": false
  }, {
    id: 37,
    name: "London",
    "sortOrder": 0,
    "isCapital": true
  }]
}, {
  "name": "Hackem",
  id: 2,
  "cities": [{
    id: 1,
    name: "Somecity",
    "sortOrder": 1,
    "isCapital": false
  }, {
    id: 2,
    name: "Waterton",
    "sortOrder": 2,
    "isCapital": false
  }, {
    id: 3,
    name: "Acre City",
    "sortOrder": 3,
    "isCapital": false
  }, {
    id: 4,
    name: "Jackson",
    "sortOrder": 4,
    "isCapital": false
  }, {
    id: 5,
    name: "Tolkenshire",
    "sortOrder": 6,
    "isCapital": false
  }, {
    id: 6,
    name: "Capital City",
    "sortOrder": 0,
    "isCapital": true
  }, {
    id: 37,
    name: "Paris",
    "sortOrder": 4,
    "isCapital": false
  }]
}, {
  "name": "NewCountry",
  id: 3,
  "cities": [{
    id: 1,
    name: "Skycity",
    "sortOrder": 1,
    "isCapital": false
  }, {
    id: 2,
    name: "DirtCity",
    "sortOrder": 2,
    "isCapital": false
  }, {
    id: 3,
    name: "Airville",
    "sortOrder": 3,
    "isCapital": false
  }, {
    id: 6,
    name: "Cape Town",
    "sortOrder": 0,
    "isCapital": true
  }, {
    id: 37,
    name: "Walla Walla",
    "sortOrder": 4,
    "isCapital": false
  }]
}];

Add some functions to namespace to use:

myApp.arrayObj = myApp.arrayObj || {
  lookup: function(myArray, searchTerm, property, firstOnly) {
    var found = [];
    for (var i = 0; i < myArray.length; i++) {
      if (myArray[i][property] === searchTerm) {
        found.push(myArray[i]);
        if (firstOnly) break; //if only the first 
      }
    }
    return found;
  },
  updateCityOptions: function(countries) {
    var $cityAll = $("#ListingCity");
    // lookup by text
    var $countryText = $("#ListingCountry option:selected").text();
    var ac = this.lookup(countries, $countryText, "name", true)[0].cities;
    // lookup by id (alternate approach)
    // var $countryId = $("#ListingCountry option:selected").val();
    // var ac = this.lookup(countries, $countryId , "id", true)[0].cities;
    if (ac) {
      $('#ListingCity option:gt(0)').remove();
      var opt = '<option></option>';
      var newOptions = "";
      $.each(ac, function(k2, v2) {
       // var sel = currentCity == v2.name ? ' selected="selected" ' : "";
        var sel = v2.isDefault ? ' selected="selected" ' : "";
        opt = '<option data-sort="' + v2.sortOrder + '"' + sel + ' value="' + v2.id + '">' + v2.name + '</option>';
        newOptions += opt;
      });
      $cityAll.append(newOptions); //hit the DOM just once
      $cityAll.sortOptions(); // sort what we put in
      $(".city").fadeIn(300);
    } else {
      $cityAll.find('option:gt(0)').remove();
      $(".city").fadeOut(300);
    }
    $cityAll.trigger('change');// because it changed
  },
  setCountries: function(nation) {
    var $countries = $("#ListingCountry");
    $countries.find('option:gt(0)').remove();
    var opt = '';
    var newOptions = "";
    $.each(nation, function(k2, v2) {
      var sel = v2.isDefault ? ' selected="selected" ' : "";
      opt = '<option data-sort="' + v2.sortOrder + '"' + sel + ' value="' + v2.id + '">' + v2.name + '</option>';
      newOptions += opt;
    });
    $countries.append(newOptions); //hit the DOM just once
  }
};

Create the key sort function:

// sort the select
$.fn.sortOptions = function() {
  $(this).each(function() {
    var op = $(this).children("option");
    op.sort(function(a, b) {
      return $(a).data('sort') > $(b).data('sort') ? 1 : -1;
    })
    return $(this).empty().append(op);
  });
}

Startup code:

myApp.arrayObj.setCountries(myApp.countries);
// could be done is way  but we trigger the country change which does this
// myApp.arrayObj.updateCityOptions(myApp.countries);

Handle the country change event:

$('#ListingCountry').on('change', function() {
  myApp.arrayObj.updateCityOptions(myApp.countries);
}).trigger('change');

Edit: Note you could remove all the "isCapital": false if you like only keeping the "isCapital": true on one and it would work the same way.

Sample to play with here: https://jsfiddle.net/MarkSchultheiss/okk22ovq/2/

EDIT: Bonus; alternate sort options

// sort the select
$.fn.sortOptionsByText = function() {
  $(this).each(function() {
    var op = $(this).children("option");
    op.sort(function(a, b) {
      return a.text > b.text ? 1 : -1;
    })
    return $(this).empty().append(op);
  });
}

// sort the select
$.fn.sortOptionsByValue = function() {
  $(this).each(function() {
    var op = $(this).children("option");
    op.sort(function(a, b) {
      return a.value > b.value ? 1 : -1;
    })
    return $(this).empty().append(op);
  });
}

// sort the select **IF** we had a "mostused" data-mostused value
// sort the select
$.fn.sortOptions = function() {
  $(this).each(function() {
    var op = $(this).children("option");
    op.sort(function(a, b) {
      return $(a).data('mostused') > $(b).data('mostused') ? 1 : -1;
    })
    return $(this).empty().append(op);
  });
}
Mark Schultheiss
  • 28,892
  • 9
  • 63
  • 88