0

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:

  • select Blue/Red or Green color
  • click on Reduced to only have Black and White options
  • See that the model value is left to blank

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>
2
  • Well, I am betting that object is set to undefined, so it has nothing to track by. When you click the colors reduced I would call a function and then set your colors to a blank color of some sort, I would not handle the code the html, I would handle it in the controller... can you have color that is 'blank" Commented Jan 30, 2017 at 22:44
  • I bet it trying to find that new color by the id and that id has been removed so it finds undefined... Commented Jan 30, 2017 at 22:45

3 Answers 3

1

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

Sign up to request clarification or add additional context in comments.

2 Comments

This is exactly what I needed. Except that ng-change is not called at init, and therefore the previousColor is not set and it bugs if you press reduce before. I will write another answer with a possible complement. Feel free to incorporate it in your answer. If you do I will select your answer.
You're right, previousColor was not set if you click on the Reduce button. I added a check to make sure previousColor is defined before running the loop.
0

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.

1 Comment

I'm sorry but I do not see how this does what I want. It replaces the value with colorsReduced[0]. Please look at @Jukebox's answer
0

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);
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.