Tuesday, March 8, 2016

Angular Recipe: Avoid empty option in HTML select element

HTML select in Angular JS


HTML select element is often used to create a drop-down list.
You could use either ng-repeat or ng-options directive to fulfill it with options.
Note that the value of a select directive used without ngOptions is always a string.
That's why i prefer to use ng-options directive rather than ng-repeat one.
Note: if ng-model value is undefined, has a wrong type or is not contained among the options set, than selected element into your drop-down would be empty.
Note: It also might happen when ng-model variable is distinct into the child-scope from the one that u've defined in JS.
If you are working with objects that have an identifier property, you should track by the identifier instead of the whole object.

Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance. If you don't have a unique identifier, track by $index can also provide a performance boost.
According to (Prototypical inheritance) your directive might create a child-scope, which could distinct your ng-model="fooBar" variable and no longer refer up to the parent’s $scope.fooBar variable.

To not storing the data directly on the scope helps in this case.

Implementation with ngRepeat

app.js

JavaScript
var app = angular.module('amApp', []);
app.controller('MainCtrl', function($scope, $filter) {

$scope.fruits = [
  {id:1, name:'apple', price:0.65},
  {id:2, name:'orange', price:0.75}
];
$scope.selectedFruitId = $scope.fruits[0].id.toString();  // 0_o

*.html

HTML
<div ng-controller="MainCtrl">
  <h3>Implementation with <code>ngRepeat</code>:</h3>
  <label>fruits:</label>
  <select class="form-control" ng-model="selectedFruitId">
    <option ng-repeat="fruit in fruits" value="{{fruit.id}}">{{fruit.name}}</option>
  </select>
</div>

Empty selected option in select element pitfalls

Wrong type

Watch-out, type-casting is required in the following line:

$scope.selectedFruitId = $scope.fruits[0].id.toString();

Because id is integer, but string is required, if you'll write just:

$scope.selectedFruitId = $scope.fruits[0].id;

you'll have empty selected element:

<option value="? number:1 ?"></option>

Empty/Undefined value

If you will not even declare model variable,
you'll have the following empty selected element:

<option value="? undefined:undefined ?"></option>

If you will declare model variable but forget to init it (e.g. $scope.selectedFruitId = null;)
you'll have the following empty selected element:

<option value="? object:null ?"></option>

In case with not specified string (e.g. $scope.selectedFruitId = "";)
you'll have the following empty selected element:

<option value="? string: ?"></option>

Value is not among the options:

In case with string type (e.g. $scope.selectedFruitId = "27";) you'll have the following empty selected element:

<option value="? string:27 ?"></option>

Prevent empty first element in your drop-down

You need to specify default either in *.js or via ng-init:

  • In JS: $scope.selectedFruitId = $scope.fruits[0].id.toString();
  • Via ngInit: ng-init="selectedFruitId = fruits[0].id.toString()"

Implementation with ngOptions

In many cases, ngRepeat can be used on <option> elements instead of ngOptions to achieve a similar result. However, ngOptions provides some benefits, such as more flexibility in how the <select>'s model is assigned via the select as part of the comprehension expression, and additionally in reducing memory and increasing speed by not creating a new scope for each repeated instance.

Other words, the advantage is, that you can simply use the whole object as a selected value:

app.js

JavaScript
$scope.vegetables = [
  {id:3, name:'potato', price:0.25},
  {id:4, name:'tomato', price:0.5}
];
$scope.selectedVegetable = $scope.vegetables[0];  // ^_^

*.html

HTML
<div ng-controller="MainCtrl">
  <h3>Implementation with <code>ngOptions</code>:</h3>
  <label>vegetables:</label>
  <select class="form-control" ng-model="selectedVegetable" ng-options="vegetable as vegetable.name for vegetable in vegetables track by vegetable.id"></select>
</div>

Note: selectedVegetable is an object, but options are still tracked by id:

(track by vegetable.id is basically the same as value="{{vegetable.id}}" in our implementation with ngRepeat)


Understanding Scopes (Prototypical inheritance)

In most cases, directives and scopes interact but do not create new instances of scope. However, some directives, such as ng-controller and ng-repeat, create new child scopes and attach the child scope to the corresponding DOM element.

Scopes can be nested to limit access to the properties of application components while providing access to shared model properties. Nested scopes are either "child scopes" or "isolate scopes". A "child scope" (prototypically) inherits properties from its parent scope. An "isolate scope" does not.

When new scopes are created, they are added as children of their parent scope. When Angular evaluates {{name}}, it first looks at the scope associated with the given element for the name property. If no such property is found, it searches the parent scope and so on until the root scope is reached. In JavaScript this behavior is known as prototypical inheritance, and child scopes prototypically inherit from their parents.


Empty selected option in select element because of wrong Scope

According to (Prototypical inheritance) your directive might create a child-scope, which could distinct your ng-model="fooBar" variable and no longer refer up to the parent’s $scope.fooBar variable.

Consequences are, that your $scope.fooBar variable remains the same regardless of the selected option, but relevant fooBar variable exist in the child-scope.

So, instead of this:

// Controller 
$scope.username= "Joe"

// HTML
<input ng-model="username">

Do this:

// Controller 
$scope.user = {name: "Joe"};

// HTML
<input ng-model="user.name">

Now when ng-model wants to write, it firstly reads, then writes. It reads the user property from the element scope, and it works because a child-scope will search upwards for a read. Once it finds user in parent-scope, it successfully updates the user.name.


Handle the select element option change with ngChange directive

Below is an implementation of two related drop-downs.
The second drop-down shows either fruits or vegetables list depending of the specified category into the first drop-down:

app.js

JavaScript
...
$scope.groups = [$scope.fruits, $scope.vegetables];
$scope.categories = [
  {id:5, name:'fruits', groupId:0},
  {id:7, name:'vegetables', groupId:1}
];
$scope.selectedCategory = $scope.categories[0];

$scope.onCategoryChange = function(selectedItem) {
  $log.log(selectedItem);
  $log.log($scope.selectedCategory === selectedItem);
  $scope.products = $scope.groups[selectedItem.groupId];
  $scope.selectedProduct = $scope.products[0];
}
$scope.onCategoryChange($scope.selectedCategory);

$scope.getFullDescription = function(item) {
  return item.name + ': ' + $filter('currency')(item.price);  // bonus:)
};

*.html

HTML
<div ng-controller="MainCtrl">
  <h3>Implementation with Change handling:</h3>
  <label>categories:</label>
  <select class="form-control" ng-model="selectedCategory" ng-change=onCategoryChange(selectedCategory); ng-options="category as category.name for   category in categories track by category.id">
  </select>
  <label>products:</label>
  <select class="form-control" ng-model="selectedProduct" ng-options="product as getFullDescription(product) for product in products track by product.id"></select>
</div>

Watchout: In case of distinct ng-model variable issue, even if u'll try to set selected item manually on change:

JavaScript
$scope.onCategoryChange = function(selectedItem) {
  $scope.selectedCategory = selectedItem;
}

you might have an extra empty selected option in to your <select> element, which will shadow the actual problem reason.

Hope it was deep enough to help U to solve your problem, with which I've faced couple of times and dig among a lot of resources to solve it.


see Also


6 comments:

  1. Thank you for your very helpful post! Help me through the select being blank issue that I struggled with for quite sometime. Keep it up!

    ReplyDelete
  2. I really like and appreciate your post. Really thank you! Fantastic.
    python training
    angular js training
    selenium trainings

    ReplyDelete
  3. Great post.I'm glad to see people are still interested of Article.Thank you for an interesting read........
    Offshore Angularjs 2 Developer in India

    ReplyDelete
  4. Great blog the content is informative and engaging. Visit my website to get best Information
    Frozen Shoulder Treatment in Surrey

    ReplyDelete
  5. Wonderful blog with great piece of information. Visit my website to get best Information
    vintage oval mirror

    ReplyDelete