Search code examples
javascripthtmlangularjsangularjs-filterangularjs-ng-options

Alphabetize Options from Object Preserving Key/Value Associations in AngularJS


What I need, is to be able to sort a select element's child option nodes by their text, while maintaining their value associations. I can either get them to alphabetize and disassociate from their keys, or I can associate their keys without alphabetization.

I know there is a method for accessing JSON directly, but this is a phased implementation, and that functionality will come later. For now, I must load the json via precursory script block to when the app script MyInterface.js is loaded.

I've researched custom filters, but that's where the disassociation from the keys comes from. If the JSON needs to be in an entirely different format, I'd be willing to adopt it.

Here's what I've got...

HTML

<!doctype html>
<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js"></script>
  </head>
  <body>
    <div ng-app="MyInterface" ng-controller="SimpleJSON">
      <select name="index_category"
        ng-model="IndexCategory"
        ng-init="IndexCategory = IndexCategory || '0'"
        ng-options="option in JSON.Categories | orderObjectBy:'val'">
        <option value="0">Select Animal...</option>
      </select>
    </div>
    <script>
      var json = {
        "123":"Zebra",
        "456":"Monkey",
        "789":"Anteater"
      };
    </script>
    <script src="MyInterface.js"></script>
  </body>
</html>

MyInterface.js

(function(json) {

  // AngularJS Module
  var MyInterface = angular.module('MyInterface', []);

  // Custom Filters
  MyInterface.filter('orderObjectBy', function() {
    return function(items, field, reverse) {
      var filtered = [];
      angular.forEach(items, function(item) {
        filtered.push(item);
      });
      filtered.sort(function(a, b) {
        return a[field] > b[field] ? 1 : -1;
      });
      if(reverse) filtered.reverse();
      return filtered;
    };
  });

  // Controllers
  MyInterface.controller('SimpleJSON', function($scope) {
    $scope.JSON = angular.fromJson(json);
  });

})(json);

Essentially what the selection needs to have option-wise is:

<option value="0">Select Animal...</option>
<option value="789">Anteater</option>
<option value="456">Monkey</option>
<option value="123">Zebra</option>

Alterations Incorporating Accepted Answer

HTML

<!doctype html>
<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js"></script>
  </head>
  <body>
    <div ng-app="MyInterface" ng-controller="SimpleJSON">
      <select name="index_category"
        ng-model="IndexCategory"
        ng-options="option.value for option in JSON track by option.key">
        <option value="">Select Animal...</option>
      </select>
    </div>
    <script>
      var json = [
        { key:"123", value:"Zebra" },
        { key:"456", value:"Monkey" },
        { key:"789", value:"Anteater" }
      ];
    </script>
    <script src="MyInterface.js"></script>
  </body>
</html>

MyInterface.js

(function(json) {

  // AngularJS Module
  var MyInterface = angular.module('MyInterface', []);

  // Controllers
  MyInterface.controller('SimpleJSON', function($scope, orderByFilter) {

    // Load JSON
    $scope.JSON = angular.fromJson(json);

    // Alphabetize by Value
    $scope.JSON = orderByFilter($scope.JSON, 'value');

  });

})(json);

Solution

  • You need to use array of objects with ng-options. i.e , Example

     $scope.animals= [
        {key:"123", value:"Zebra"},
        {key:"456",value: "Monkey"},
        {key:"789", value: "Anteater"}
      ];
    

    and since you only want key in the ng-model you would need to use syntax ng-options="option.key as option.value for option in animals | orderBy:'value'". And for default option just provide an option with value=""

    <select name="index_category" ng-model="IndexCategory" ng-options="option.key as option.value for option in animals| orderBy:'value'">
        <option value="">Select Animal...</option>
    </select>
    

    Demo

    angular.module('app', []).controller('ctrl', function($scope) {
      $scope.JSON = [{
        key: "123",
        value: "Zebra"
      }, {
        key: "456",
        value: "Monkey"
      }, {
        key: "789",
        value: "Anteater"
      }];
    
    });
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <div ng-app="app" ng-controller="ctrl">
      <select name="index_category" ng-model="IndexCategory" ng-options="option.key as option.value for option in JSON | orderBy:'value'">
        <option value="">Select Animal...</option>
      </select>
      {{IndexCategory}}
    </div>

    Also you need to remember DOM filters are expensive as they run twice atleast during the digest cycle. If your select list is dynamic do not place DOM filter, instead you could just sort it in your controller before binding the data. In your case you could just either use a simple native sort or the orderBy filter itself in the controller.

    i.e

    angular.module('app', []).controller('ctrl', function($scope, orderByFilter) {
      $scope.animals= [{
        key: "123",
        value: "Zebra"
      }, {
        key: "456",
        value: "Monkey"
      }, {
        key: "789",
        value: "Anteater"
      }];
      /*Using orderByFilter*/
      $scope.animals= orderByFilter($scope.JSON, 'value');
      /*Or just use native sort*/
      //$scope.animals.sort(function(a, b){ return a['value'] > b['value'] });
    
    });
    

    and your view would just be:

     ng-options="option.key as option.value for option in animals">
    

    If you do not have an option to get the data in array format from the server and you need to transform, just do it in your controller like:

    $scope.animals = convertToOptions(optionsObject);
    
    function convertToOptions(obj){
        var options = [];
        angular.forEach(obj, function(value, key){
            options.push({key:key, value:value});
        });
        return options;
      }
    

    If you are really concerned about setting the option value same as that of the key property you would need to use track by. i.e

      ng-options="option.value for option in animals track by option.key">
    

    Side Note: Do not use ng-init with ng-options.

    From documentation:

    The only appropriate use of ngInit is for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.