Search code examples
javascriptangularjsangularjs-scopeangularjs-ng-modelangularjs-ng-options

Default select not working in ngoptions when model initialized with ng-init


I have a cascading select with 2nd dropdown appears based on the first dropdown selection. An extra blank option appears which is not part of intended behaviour of the dropdown.

<select ng-init="order.attempt_status_sub = order.attempt_status_sub || subStatuses[0].name" 
    data-ng-model="order.attempt_status_sub" ng-options="subStatus.name as 
    subStatus.name for subStatus in subStatuses"> 
</select>

How do I avoid empty extra select from appearing in dropdown?

enter image description here

code for my cascading dropdowns is

    <div class="form-group" ng-class="{ 'has-error' : submitted && orderForm.content.$invalid}">
      <div class="controls">
        <select ng-change="getSubStatuses(order.attempt_status)" data-ng-model="order.attempt_status" ng-options="status.name as status.name for status in statuses">
        </select>
      </div>
    </div>

    <div class="form-group" ng-show="subStatuses.length" ng-class="{ 'has-error' : submitted && orderForm.content.$invalid}">
      <div class="controls">
        <select ng-init="order.attempt_status_sub = order.attempt_status_sub || subStatuses[0].name" data-ng-model="order.attempt_status_sub" ng-options="subStatus.name as subStatus.name for subStatus in subStatuses">
        </select>
      </div>
    </div>
$scope.getSubStatuses = function(attempt_status) {

    var statuses = $scope.statuses;
    for(var index in statuses) {
        if(statuses[index].name === attempt_status) {

            if(statuses[index].children) {
                $scope.subStatuses = statuses[index].children;
            } else {
                $scope.subStatuses = [];
            }
            break;
        }
    }
};

Solution

  • I am assuming you are setting this for an asynchronously bound data. ng-init is really bad for this purpose. Thumb rule is do not use ng-init for something which controller is supposed to do. You should set up your view model in the controller. The reason why it could fail when the data is bound asynchronously is because ng-init'ed expression is not watched. So it just runs once during the first rendering and the subStatuses is not yet populated and it falls back to showing the default empty option. And even if the data is populated and view is updated during the later digest cycle ng-init expression does not run again.

    From Doc

    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.

    So just initialize it in your controller, instead of using ng-init.

    $scope.getSubStatuses = function(attempt_status) {
    
        var statuses = $scope.statuses;
        for(var index in statuses) {
            if(statuses[index].name === attempt_status) {
    
                if(statuses[index].children) {
                    $scope.subStatuses = statuses[index].children;
                } else {
                    $scope.subStatuses = [];
                }
                break;
            }
        }
        //Initialize the model here, assuming $scope.order is already initialized
        $scope.order.attempt_status_sub = ($scope.subStatuses[0] ||{}).name;
    };
    

    angular.module('app', []).controller('ctrl', function($scope, $timeout) {
    
      $timeout(function() {
        $scope.subStatuses = [{
          name: 'test1'
        }, {
          name: 'test2'
        }, {
          name: 'test3'
        }];
    
        $scope.order = {
          attempt_status_sub1: $scope.subStatuses[0].name
        };
    
      });
    
    
    
    });
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <div ng-app="app" ng-controller="ctrl">
      With Ng-Init
      <select ng-init="order.attempt_status_sub = order.attempt_status_sub || subStatuses[0].name" data-ng-model="order.attempt_status_sub" ng-options="subStatus.name as 
        subStatus.name for subStatus in subStatuses">
      </select>
    
      With controller initialization.
    
      <select data-ng-model="order.attempt_status_sub1" ng-options="subStatus.name as 
        subStatus.name for subStatus in subStatuses">
      </select>
    </div>