Benoît
Benoît

Reputation: 1100

Keep selected option when new options array does not contain the value

I have a select with ngOptions based on an array. This array can change.

If the new array value does not contain the selected option value, the option value is set to undefined by the selectController. Is there a way to prevent this ?

Plunker : https://plnkr.co/edit/kao3h5ivHXlP1Wrdx1Ib?p=preview

Scenario:

Wanted behavior : that the model value stays at Blue/Red or Green

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="//code.angularjs.org/snapshot/angular.min.js"></script>
</head>
<body ng-app="selectExample">
  <script>
angular.module('selectExample', [])
  .controller('ExampleController', ['$scope', function($scope) {
    $scope.colorsFull = [
      {id:"bk", name:'black'},
      {id:"w", name:'white'},
      {id:"r", name:'red'},
      {id:"be", name:'blue'},
      {id:"y", name:'yellow'}
    ];
    $scope.colors = $scope.colorsFull;
    $scope.selectedColor =$scope.colorsFull[0];
    $scope.colorsReduced = [
      {id:"bk", name:'black2'},
      {id:"w", name:'white2'}];
  }]);
</script>
<div ng-controller="ExampleController">
  <button ng-click="colors=colorsReduced">Reduced</button>
  <button ng-click="colors=colorsFull">Full</button>
  <br/>
  Colors : {{colors}} 
  <hr/>
  <select ng-model="selectedColor" ng-options="color.name for color in colors track by color.id">
  </select>
  selectedColor:{{selectedColor}}

</div>
</body>
</html>

Upvotes: 0

Views: 1021

Answers (3)

Jukebox
Jukebox

Reputation: 1603

You can achieve this by keeping track of what color is selected in the full colors dropdown, and inserting it into the reduced colors array. First, add an ng-change directive so that we can keep track of the selected color:

<select ng-model="selectedColor" ng-options="color.name for color in colors track by color.id" ng-change="setColor(selectedColor)">

And in your controller:

$scope.setColor = function(color) {
    if(color !== null) {
        // Keep track of the color that is selected
        $scope.previousColor = color;
    }
    else {
        // Changed arrays, keep selected color in model
        $scope.selectedColor = $scope.previousColor;
    }
}  

Now ng-model is set to the correct color whenever the arrays are changed, but it will appear blank in the reduced colors dropdown because the option doesn't exist. So, we need to insert that option into the array. However, switching back and forth between dropdowns will cause the reduced colors array to keep on adding more options, and we only want to remember the option we selected from the full colors array. So, we need create an initial set of colors to revert back to when switching.

// Keep a copy of the original set of reduced colors
$scope.colorsReducedInitial = [
    {id:"bk", name:'black2'},
    {id:"w", name:'white2'}];

Finally, we need to insert the selected option into the reduced colors array. Change the ng-click on the Reduced button to use a function:

<button ng-click="setColorsReduced()">Reduced</button>

Now, we can insert the option, after resetting the reduced colors array to its initial state:

$scope.setColorsReduced = function() {
    // Revert back to the initial set of reduced colors
    $scope.colors = angular.copy($scope.colorsReducedInitial);

    if($scope.previousColor !== undefined) {
        var found = false;
        angular.forEach($scope.colorsReducedInitial, function(value, key) {
            if(value.id == $scope.previousColor.id) {
                found = true;
            }
        });

        // If the id is found, no need to push the previousColor
        if(!found) {
            $scope.colors.push($scope.previousColor);
        }
    }
}

Note that we are looping through the reduced colors array to ensure we aren't duplicating any colors, such as black or white.

Now, the reduced colors ng-model has the previous dropdown's selected color.

Updated Plunkr Demo

Upvotes: 1

Beno&#238;t
Beno&#238;t

Reputation: 1100

Using Jukebox's answer, I ended-up writing a directive, using the modelCtrl.$formatters to get the initial value. It also offer the possibility to store the previousValue in the scope or in a local variable :

Usage: <select .... select-keep> or <select .... select-keep="previousColor">

Directive:

 .directive('selectKeep', function($parse) {
    return {
      require: 'ngModel',
      link: function (scope, element, attrs, modelCtrl) {
        var previousValueGetter;
        var previousValueSetter;
        if (attrs.selectKeep) { //use a scope attribute to store the previousValue
              previousValueGetter = $parse(attrs.selectKeep);
              previousValueSetter = previousValueGetter.assign;
        }
        else { //use a local variable to store the previousValue
              var previousValue;
              previousValueGetter = function(s) { return previousValue;};
              previousValueSetter = function(s, v) { previousValue = v;};
        }

        //store the initial value
        modelCtrl.$formatters.push(function(v) {
              previousValueSetter(scope, v);
          return v;
        });

        //get notified of model changes (copied from Jukebox's answer)
        modelCtrl.$viewChangeListeners.push(function() {
          if (modelCtrl.$modelValue !== null) {
            previousValueSetter(scope, modelCtrl.$modelValue);
          } else {
            modelCtrl.$setViewValue(previousValueGetter(scope));
          }
        });
      }
    };

Plunker

Edit : it has a flaw, the form gets dirty even if the value does not change. I had to add these lines in the else of the viewChangeListener but it doesn't look nice. Any ideas ?:

...
} else {
  modelCtrl.$setViewValue(previousValueGetter(scope));
  //set pristine since this change is not a real change
  modelCtrl.$setPristine(true);
  //check if any other modelCtrl is dirty. If not, we will have to put the form as pristine too
  var oneDirty =_.findKey(modelCtrl.$$parentForm, function(otherModelCtrl) {
    return otherModelCtrl && otherModelCtrl.hasOwnProperty('$modelValue') && otherModelCtrl !== modelCtrl && otherModelCtrl.$dirty;
  });
  if (!oneDirty) {
   modelCtrl.$$parentForm.$setPristine(true);
  }
}

Upvotes: 0

Abhishekkumar
Abhishekkumar

Reputation: 1102

Use below code in script

$scope.makeSelected=function(){
    $scope.selectedColor =$scope.colorsReduced[0];
  }

And just add this function call in reduced button line like below

<button ng-click="colors=colorsReduced;makeSelected()">Reduced</button>

This will do what you want to achieve.

Upvotes: 0

Related Questions