diff --git a/angularFiles.js b/angularFiles.js
index d0b0d8dd8504..696bd8e78ec8 100755
--- a/angularFiles.js
+++ b/angularFiles.js
@@ -65,6 +65,7 @@ var angularFiles = {
     'src/ng/directive/ngList.js',
     'src/ng/directive/ngModel.js',
     'src/ng/directive/ngNonBindable.js',
+    'src/ng/directive/ngOptions.js',
     'src/ng/directive/ngPluralize.js',
     'src/ng/directive/ngRepeat.js',
     'src/ng/directive/ngShowHide.js',
diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js
new file mode 100644
index 000000000000..f6ec5f227c0b
--- /dev/null
+++ b/src/ng/directive/ngOptions.js
@@ -0,0 +1,612 @@
+'use strict';
+
+/* global jqLiteRemove */
+
+var ngOptionsMinErr = minErr('ngOptions');
+
+/**
+ * @ngdoc directive
+ * @name ngOptions
+ * @restrict A
+ *
+ * @description
+ *
+ * The `ngOptions` attribute can be used to dynamically generate a list of `<option>`
+ * elements for the `<select>` element using the array or object obtained by evaluating the
+ * `ngOptions` comprehension expression.
+ *
+ * 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 reducing memory and
+ * increasing speed by not creating a new scope for each repeated instance, as well as providing
+ * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound
+ *  to a non-string value. This is because an option element can only be bound to string values at
+ * present.
+ *
+ * When an item in the `<select>` menu is selected, the array element or object property
+ * represented by the selected option will be bound to the model identified by the `ngModel`
+ * directive.
+ *
+ * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
+ * be nested into the `<select>` element. This element will then represent the `null` or "not selected"
+ * option. See example below for demonstration.
+ *
+ * <div class="alert alert-warning">
+ * **Note:** `ngModel` compares by reference, not value. This is important when binding to an
+ * array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/).
+ * </div>
+ *
+ * ## `select` **`as`**
+ *
+ * Using `select` **`as`** will bind the result of the `select` expression to the model, but
+ * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources)
+ * or property name (for object data sources) of the value within the collection. If a **`track by`** expression
+ * is used, the result of that expression will be set as the value of the `option` and `select` elements.
+ *
+ *
+ * ### `select` **`as`** and **`track by`**
+ *
+ * <div class="alert alert-warning">
+ * Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
+ * </div>
+ *
+ * Consider the following example:
+ *
+ * ```html
+ * <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
+ * ```
+ *
+ * ```js
+ * $scope.values = [{
+ *   id: 1,
+ *   label: 'aLabel',
+ *   subItem: { name: 'aSubItem' }
+ * }, {
+ *   id: 2,
+ *   label: 'bLabel',
+ *   subItem: { name: 'bSubItem' }
+ * }];
+ *
+ * $scope.selected = { name: 'aSubItem' };
+ * ```
+ *
+ * With the purpose of preserving the selection, the **`track by`** expression is always applied to the element
+ * of the data source (to `item` in this example). To calculate whether an element is selected, we do the
+ * following:
+ *
+ * 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]`
+ * 2. Apply **`track by`** to the already selected value in `ngModel`.
+ *    In the example: this is not possible as **`track by`** refers to `item.id`, but the selected
+ *    value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to
+ *    a wrong object, the selected element can't be found, `<select>` is always reset to the "not
+ *    selected" option.
+ *
+ *
+ * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} required The control is considered valid only if value is entered.
+ * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
+ *    the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
+ *    `required` when you want to data-bind to the `required` attribute.
+ * @param {comprehension_expression=} ngOptions in one of the following forms:
+ *
+ *   * for array data sources:
+ *     * `label` **`for`** `value` **`in`** `array`
+ *     * `select` **`as`** `label` **`for`** `value` **`in`** `array`
+ *     * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ *     * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
+ *     * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
+ *        (for including a filter with `track by`)
+ *   * for object data sources:
+ *     * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ *     * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ *     * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ *     * `select` **`as`** `label` **`group by`** `group`
+ *         **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ *
+ * Where:
+ *
+ *   * `array` / `object`: an expression which evaluates to an array / object to iterate over.
+ *   * `value`: local variable which will refer to each item in the `array` or each property value
+ *      of `object` during iteration.
+ *   * `key`: local variable which will refer to a property name in `object` during iteration.
+ *   * `label`: The result of this expression will be the label for `<option>` element. The
+ *     `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
+ *   * `select`: The result of this expression will be bound to the model of the parent `<select>`
+ *      element. If not specified, `select` expression will default to `value`.
+ *   * `group`: The result of this expression will be used to group options using the `<optgroup>`
+ *      DOM element.
+ *   * `trackexpr`: Used when working with an array of objects. The result of this expression will be
+ *      used to identify the objects in the array. The `trackexpr` will most likely refer to the
+ *     `value` variable (e.g. `value.propertyName`). With this the selection is preserved
+ *      even when the options are recreated (e.g. reloaded from the server).
+ *
+ * @example
+    <example module="selectExample">
+      <file name="index.html">
+        <script>
+        angular.module('selectExample', [])
+          .controller('ExampleController', ['$scope', function($scope) {
+            $scope.colors = [
+              {name:'black', shade:'dark'},
+              {name:'white', shade:'light'},
+              {name:'red', shade:'dark'},
+              {name:'blue', shade:'dark'},
+              {name:'yellow', shade:'light'}
+            ];
+            $scope.myColor = $scope.colors[2]; // red
+          }]);
+        </script>
+        <div ng-controller="ExampleController">
+          <ul>
+            <li ng-repeat="color in colors">
+              Name: <input ng-model="color.name">
+              [<a href ng-click="colors.splice($index, 1)">X</a>]
+            </li>
+            <li>
+              [<a href ng-click="colors.push({})">add</a>]
+            </li>
+          </ul>
+          <hr/>
+          Color (null not allowed):
+          <select ng-model="myColor" ng-options="color.name for color in colors"></select><br>
+
+          Color (null allowed):
+          <span  class="nullable">
+            <select ng-model="myColor" ng-options="color.name for color in colors">
+              <option value="">-- choose color --</option>
+            </select>
+          </span><br/>
+
+          Color grouped by shade:
+          <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
+          </select><br/>
+
+
+          Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br>
+          <hr/>
+          Currently selected: {{ {selected_color:myColor} }}
+          <div style="border:solid 1px black; height:20px"
+               ng-style="{'background-color':myColor.name}">
+          </div>
+        </div>
+      </file>
+      <file name="protractor.js" type="protractor">
+         it('should check ng-options', function() {
+           expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
+           element.all(by.model('myColor')).first().click();
+           element.all(by.css('select[ng-model="myColor"] option')).first().click();
+           expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
+           element(by.css('.nullable select[ng-model="myColor"]')).click();
+           element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
+           expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
+         });
+      </file>
+    </example>
+ */
+
+// jshint maxlen: false
+                         //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
+var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
+                        // 1: value expression (valueFn)
+                        // 2: label expression (displayFn)
+                        // 3: group by expression (groupByFn)
+                        // 4: array item variable name
+                        // 5: object item key variable name
+                        // 6: object item value variable name
+                        // 7: collection expression
+                        // 8: track by expression
+// jshint maxlen: 100
+
+
+var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
+
+  function parseOptionsExpression(optionsExp, selectElement, scope) {
+
+    var match = optionsExp.match(NG_OPTIONS_REGEXP);
+    if (!(match)) {
+      throw ngOptionsMinErr('iexp',
+        "Expected expression in form of " +
+        "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
+        " but got '{0}'. Element: {1}",
+        optionsExp, startingTag(selectElement));
+    }
+
+    // Extract the parts from the ngOptions expression
+
+    // The variable name for the value of the item in the collection
+    var valueName = match[4] || match[6];
+    // The variable name for the key of the item in the collection
+    var keyName = match[5];
+
+    // An expression that generates the viewValue for an option if there is a label expression
+    var selectAs = / as /.test(match[0]) && match[1];
+    // An expression that is used to track the id of each object in the options collection
+    var trackBy = match[8];
+    // An expression that generates the viewValue for an option if there is no label expression
+    var valueFn = $parse(match[2] ? match[1] : valueName);
+    var selectAsFn = selectAs && $parse(selectAs);
+    var viewValueFn = selectAsFn || valueFn;
+    var trackByFn = trackBy && $parse(trackBy);
+
+    // Get the value by which we are going to track the option
+    // if we have a trackFn then use that (passing scope and locals)
+    // otherwise just hash the given viewValue
+    var getTrackByValue = trackBy ?
+                              function(viewValue, locals) { return trackByFn(scope, locals); } :
+                              function getHashOfValue(viewValue) { return hashKey(viewValue); };
+    var displayFn = $parse(match[2] || match[1]);
+    var groupByFn = $parse(match[3] || '');
+    var valuesFn = $parse(match[7]);
+
+    var locals = {};
+    var getLocals = keyName ? function(value, key) {
+      locals[keyName] = key;
+      locals[valueName] = value;
+      return locals;
+    } : function(value) {
+      locals[valueName] = value;
+      return locals;
+    };
+
+
+    function Option(selectValue, viewValue, label, group) {
+      this.selectValue = selectValue;
+      this.viewValue = viewValue;
+      this.label = label;
+      this.group = group;
+    }
+
+    return {
+      getWatchables: $parse(valuesFn, function(values) {
+        // Create a collection of things that we would like to watch (watchedArray)
+        // so that they can all be watched using a single $watchCollection
+        // that only runs the handler once if anything changes
+        var watchedArray = [];
+        values = values || [];
+
+        Object.keys(values).forEach(function getWatchable(key) {
+          var locals = getLocals(values[key], key);
+          var label = displayFn(scope, locals);
+          var selectValue = getTrackByValue(values[key], locals);
+          watchedArray.push(selectValue);
+          watchedArray.push(label);
+        });
+        return watchedArray;
+      }),
+
+      getOptions: function() {
+
+        var optionItems = [];
+        var selectValueMap = {};
+
+        // The option values were already computed in the `getWatchables` fn,
+        // which must have been called to trigger `getOptions`
+        var optionValues = valuesFn(scope) || [];
+
+        var keys = Object.keys(optionValues);
+        keys.forEach(function getOption(key) {
+
+          // Ignore "angular" properties that start with $ or $$
+          if (key.charAt(0) === '$') return;
+
+          var value = optionValues[key];
+          var locals = getLocals(value, key);
+          var viewValue = viewValueFn(scope, locals);
+          var selectValue = getTrackByValue(viewValue, locals);
+          var label = displayFn(scope, locals);
+          var group = groupByFn(scope, locals);
+          var optionItem = new Option(selectValue, viewValue, label, group);
+
+          optionItems.push(optionItem);
+          selectValueMap[selectValue] = optionItem;
+        });
+
+        return {
+          items: optionItems,
+          selectValueMap: selectValueMap,
+          getOptionFromViewValue: function(value) {
+            return selectValueMap[getTrackByValue(value, getLocals(value))];
+          }
+        };
+      }
+    };
+  }
+
+
+  // we can't just jqLite('<option>') since jqLite is not smart enough
+  // to create it in <select> and IE barfs otherwise.
+  var optionTemplate = document.createElement('option'),
+      optGroupTemplate = document.createElement('optgroup');
+
+  return {
+    restrict: 'A',
+    terminal: true,
+    require: ['select', '?ngModel'],
+    link: function(scope, selectElement, attr, ctrls) {
+
+      // if ngModel is not defined, we don't need to do anything
+      var ngModelCtrl = ctrls[1];
+      if (!ngModelCtrl) return;
+
+      var selectCtrl = ctrls[0];
+      var multiple = attr.multiple;
+
+      var emptyOption = selectCtrl.emptyOption;
+      var providedEmptyOption = !!emptyOption;
+
+      var unknownOption = jqLite(optionTemplate.cloneNode(false));
+      unknownOption.val('?');
+
+      var options;
+      var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
+
+
+      var renderEmptyOption = function() {
+        if (!providedEmptyOption) {
+          selectElement.prepend(emptyOption);
+        }
+        selectElement.val('');
+        emptyOption.prop('selected', true); // needed for IE
+        emptyOption.attr('selected', true);
+      };
+
+      var removeEmptyOption = function() {
+        if (!providedEmptyOption) {
+          emptyOption.remove();
+        }
+      };
+
+
+      var renderUnknownOption = function() {
+        selectElement.prepend(unknownOption);
+        selectElement.val('?');
+        unknownOption.prop('selected', true); // needed for IE
+        unknownOption.attr('selected', true);
+      };
+
+      var removeUnknownOption = function() {
+        unknownOption.remove();
+      };
+
+
+      selectCtrl.writeValue = function writeNgOptionsValue(value) {
+        var option = options.getOptionFromViewValue(value);
+
+        if (option) {
+          if (selectElement[0].value !== option.selectValue) {
+            removeUnknownOption();
+            removeEmptyOption();
+
+            selectElement[0].value = option.selectValue;
+            option.element.selected = true;
+            option.element.setAttribute('selected', 'selected');
+          }
+        } else {
+          if (value === null || providedEmptyOption) {
+            removeUnknownOption();
+            renderEmptyOption();
+          } else {
+            removeEmptyOption();
+            renderUnknownOption();
+          }
+        }
+      };
+
+      selectCtrl.readValue = function readNgOptionsValue() {
+
+        var selectedOption = options.selectValueMap[selectElement.val()];
+
+        if (selectedOption) {
+          removeEmptyOption();
+          removeUnknownOption();
+          return selectedOption.viewValue;
+        }
+        return null;
+      };
+
+
+      // Update the controller methods for multiple selectable options
+      if (multiple) {
+
+        ngModelCtrl.$isEmpty = function(value) {
+          return !value || value.length === 0;
+        };
+
+
+        selectCtrl.writeValue = function writeNgOptionsMultiple(value) {
+          options.items.forEach(function(option) {
+            option.element.selected = false;
+          });
+
+          if (value) {
+            value.forEach(function(item) {
+              var option = options.getOptionFromViewValue(item);
+              if (option) option.element.selected = true;
+            });
+          }
+        };
+
+
+        selectCtrl.readValue = function readNgOptionsMultiple() {
+          var selectedValues = selectElement.val() || [];
+          return selectedValues.map(function(selectedKey) {
+            var option = options.selectValueMap[selectedKey];
+            return option.viewValue;
+          });
+        };
+      }
+
+
+      if (providedEmptyOption) {
+
+        // we need to remove it before calling selectElement.empty() because otherwise IE will
+        // remove the label from the element. wtf?
+        emptyOption.remove();
+
+        // compile the element since there might be bindings in it
+        $compile(emptyOption)(scope);
+
+        // remove the class, which is added automatically because we recompile the element and it
+        // becomes the compilation root
+        emptyOption.removeClass('ng-scope');
+      } else {
+        emptyOption = jqLite(optionTemplate.cloneNode(false));
+      }
+
+      // We need to do this here to ensure that the options object is defined
+      // when we first hit it in writeNgOptionsValue
+      updateOptions();
+
+      // We will re-render the option elements if the option values or labels change
+      scope.$watchCollection(ngOptions.getWatchables, updateOptions);
+
+      // ------------------------------------------------------------------ //
+
+
+      function updateOptionElement(option, element) {
+        option.element = element;
+        if (option.value !== element.value) element.value = option.selectValue;
+        if (option.label !== element.label) {
+          element.label = option.label;
+          element.textContent = option.label;
+        }
+      }
+
+      function addOrReuseElement(parent, current, type, templateElement) {
+        var element;
+        // Check whether we can reuse the next element
+        if (current && lowercase(current.nodeName) === type) {
+          // The next element is the right type so reuse it
+          element = current;
+        } else {
+          // The next element is not the right type so create a new one
+          element = templateElement.cloneNode(false);
+          if (!current) {
+            // There are no more elements so just append it to the select
+            parent.appendChild(element);
+          } else {
+            // The next element is not a group so insert the new one
+            parent.insertBefore(element, current);
+          }
+        }
+        return element;
+      }
+
+
+      function removeExcessElements(current) {
+        var next;
+        while (current) {
+          next = current.nextSibling;
+          jqLiteRemove(current);
+          current = next;
+        }
+      }
+
+
+      function skipEmptyAndUnknownOptions(current) {
+        var emptyOption_ = emptyOption && emptyOption[0];
+        var unknownOption_ = unknownOption && unknownOption[0];
+
+        if (emptyOption_ || unknownOption_) {
+          while (current &&
+                (current === emptyOption_ ||
+                current === unknownOption_)) {
+            current = current.nextSibling;
+          }
+        }
+        return current;
+      }
+
+
+      function updateOptions() {
+
+        var previousValue = options && selectCtrl.readValue();
+
+        options = ngOptions.getOptions();
+
+        var groupMap = {};
+        var currentElement = selectElement[0].firstChild;
+
+        // Ensure that the empty option is always there if it was explicitly provided
+        if (providedEmptyOption) {
+          selectElement.prepend(emptyOption);
+        }
+
+        currentElement = skipEmptyAndUnknownOptions(currentElement);
+
+        options.items.forEach(function updateOption(option) {
+          var group;
+          var groupElement;
+          var optionElement;
+
+          if (option.group) {
+
+            // This option is to live in a group
+            // See if we have already created this group
+            group = groupMap[option.group];
+
+            if (!group) {
+
+              // We have not already created this group
+              groupElement = addOrReuseElement(selectElement[0],
+                                               currentElement,
+                                               'optgroup',
+                                               optGroupTemplate);
+              // Move to the next element
+              currentElement = groupElement.nextSibling;
+
+              // Update the label on the group element
+              groupElement.label = option.group;
+
+              // Store it for use later
+              group = groupMap[option.group] = {
+                groupElement: groupElement,
+                currentOptionElement: groupElement.firstChild
+              };
+
+            }
+
+            // So now we have a group for this option we add the option to the group
+            optionElement = addOrReuseElement(group.groupElement,
+                                              group.currentOptionElement,
+                                              'option',
+                                              optionTemplate);
+            updateOptionElement(option, optionElement);
+            // Move to the next element
+            group.currentOptionElement = optionElement.nextSibling;
+
+          } else {
+
+            // This option is not in a group
+            optionElement = addOrReuseElement(selectElement[0],
+                                              currentElement,
+                                              'option',
+                                              optionTemplate);
+            updateOptionElement(option, optionElement);
+            // Move to the next element
+            currentElement = optionElement.nextSibling;
+          }
+        });
+
+
+        // Now remove all excess options and group
+        Object.keys(groupMap).forEach(function(key) {
+          removeExcessElements(groupMap[key].currentOptionElement);
+        });
+        removeExcessElements(currentElement);
+
+        ngModelCtrl.$render();
+
+        // Check to see if the value has changed due to the update to the options
+        if (!ngModelCtrl.$isEmpty(previousValue)) {
+          var nextValue = selectCtrl.readValue();
+          if (!equals(previousValue, nextValue)) {
+            ngModelCtrl.$setViewValue(nextValue);
+          }
+        }
+      }
+
+    }
+  };
+}];
diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js
index 071a90ab1022..61079ec6050d 100644
--- a/src/ng/directive/select.js
+++ b/src/ng/directive/select.js
@@ -1,6 +1,111 @@
 'use strict';
 
-var ngOptionsMinErr = minErr('ngOptions');
+var noopNgModelController = { $setViewValue: noop, $render: noop };
+
+/**
+ * @ngdoc type
+ * @name  select.SelectController
+ * @description
+ * The controller for the `<select>` directive. This provides support for reading
+ * and writing the selected value(s) of the control and also coordinates dynamically
+ * added `<option>` elements, perhaps by an `ngRepeat` directive.
+ */
+var SelectController =
+        ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
+
+  var self = this,
+      optionsMap = new HashMap();
+
+  // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
+  self.ngModelCtrl = noopNgModelController;
+
+  // The "unknown" option is one that is prepended to the list if the viewValue
+  // does not match any of the options. When it is rendered the value of the unknown
+  // option is '? XXX ?' where XXX is the hashKey of the value that is not known.
+  //
+  // We can't just jqLite('<option>') since jqLite is not smart enough
+  // to create it in <select> and IE barfs otherwise.
+  self.unknownOption = jqLite(document.createElement('option'));
+  self.renderUnknownOption = function(val) {
+    var unknownVal = '? ' + hashKey(val) + ' ?';
+    self.unknownOption.val(unknownVal);
+    $element.prepend(self.unknownOption);
+    $element.val(unknownVal);
+  };
+
+  $scope.$on('$destroy', function() {
+    // disable unknown option so that we don't do work when the whole select is being destroyed
+    self.renderUnknownOption = noop;
+  });
+
+  self.removeUnknownOption = function() {
+    if (self.unknownOption.parent()) self.unknownOption.remove();
+  };
+
+  // Here we find the option that represents the "empty" value, i.e. the option with a value
+  // of `""`.  This option needs to be accessed (to select it directly) when setting the value
+  // of the select to `""` because IE9 will not automatically select the option.
+  //
+  // Additionally, the `ngOptions` directive uses this option to allow the application developer
+  // to provide their own custom "empty" option when the viewValue does not match any of the
+  // option values.
+  for (var i = 0, children = $element.children(), ii = children.length; i < ii; i++) {
+    if (children[i].value === '') {
+      self.emptyOption = children.eq(i);
+      break;
+    }
+  }
+
+  // Read the value of the select control, the implementation of this changes depending
+  // upon whether the select can have multiple values and whether ngOptions is at work.
+  self.readValue = function readSingleValue() {
+    self.removeUnknownOption();
+    return $element.val();
+  };
+
+
+  // Write the value to the select control, the implementation of this changes depending
+  // upon whether the select can have multiple values and whether ngOptions is at work.
+  self.writeValue = function writeSingleValue(value) {
+    if (self.hasOption(value)) {
+      self.removeUnknownOption();
+      $element.val(value);
+      if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy
+    } else {
+      if (isUndefined(value) && self.emptyOption) {
+        $element.val('');
+      } else {
+        self.renderUnknownOption(value);
+      }
+    }
+  };
+
+
+  // Tell the select control that an option, with the given value, has been added
+  self.addOption = function(value) {
+    assertNotHasOwnProperty(value, '"option value"');
+    var count = optionsMap.get(value) || 0;
+    optionsMap.put(value, count + 1);
+  };
+
+  // Tell the select control that an option, with the given value, has been removed
+  self.removeOption = function(value) {
+    var count = optionsMap.get(value);
+    if (count) {
+      if (count === 1) {
+        optionsMap.remove(value);
+      } else {
+        optionsMap.put(value, count - 1);
+      }
+    }
+  };
+
+  // Check whether the select control has an option matching the given value
+  self.hasOption = function(value) {
+    return !!optionsMap.get(value);
+  };
+}];
+
 /**
  * @ngdoc directive
  * @name select
@@ -9,12 +114,6 @@ var ngOptionsMinErr = minErr('ngOptions');
  * @description
  * HTML `SELECT` element with angular data-binding.
  *
- * # `ngOptions`
- *
- * The `ngOptions` attribute can be used to dynamically generate a list of `<option>`
- * elements for the `<select>` element using the array or object obtained by evaluating the
- * `ngOptions` comprehension expression.
- *
  * 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 reducing memory and
  * increasing speed by not creating a new scope for each repeated instance, as well as providing
@@ -27,6 +126,9 @@ var ngOptionsMinErr = minErr('ngOptions');
  * represented by the selected option will be bound to the model identified by the `ngModel`
  * directive.
  *
+ * If the viewValue contains a value that doesn't match any of the options then the control
+ * will automatically add an "unknown" option, which it then removes when this is resolved.
+ *
  * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
  * be nested into the `<select>` element. This element will then represent the `null` or "not selected"
  * option. See example below for demonstration.
@@ -36,307 +138,61 @@ var ngOptionsMinErr = minErr('ngOptions');
  * array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/).
  * </div>
  *
- * ## `select` **`as`**
- *
- * Using `select` **`as`** will bind the result of the `select` expression to the model, but
- * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources)
- * or property name (for object data sources) of the value within the collection. If a **`track by`** expression
- * is used, the result of that expression will be set as the value of the `option` and `select` elements.
- *
- *
- * ### `select` **`as`** and **`track by`**
- *
- * <div class="alert alert-warning">
- * Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
- * </div>
- *
- * Consider the following example:
- *
- * ```html
- * <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
- * ```
- *
- * ```js
- * $scope.values = [{
- *   id: 1,
- *   label: 'aLabel',
- *   subItem: { name: 'aSubItem' }
- * }, {
- *   id: 2,
- *   label: 'bLabel',
- *   subItem: { name: 'bSubItem' }
- * }];
- *
- * $scope.selected = { name: 'aSubItem' };
- * ```
- *
- * With the purpose of preserving the selection, the **`track by`** expression is always applied to the element
- * of the data source (to `item` in this example). To calculate whether an element is selected, we do the
- * following:
- *
- * 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]`
- * 2. Apply **`track by`** to the already selected value in `ngModel`.
- *    In the example: this is not possible as **`track by`** refers to `item.id`, but the selected
- *    value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to
- *    a wrong object, the selected element can't be found, `<select>` is always reset to the "not
- *    selected" option.
- *
- *
- * @param {string} ngModel Assignable angular expression to data-bind to.
- * @param {string=} name Property name of the form under which the control is published.
- * @param {string=} required The control is considered valid only if value is entered.
- * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
- *    the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
- *    `required` when you want to data-bind to the `required` attribute.
- * @param {comprehension_expression=} ngOptions in one of the following forms:
- *
- *   * for array data sources:
- *     * `label` **`for`** `value` **`in`** `array`
- *     * `select` **`as`** `label` **`for`** `value` **`in`** `array`
- *     * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
- *     * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
- *     * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
- *        (for including a filter with `track by`)
- *   * for object data sources:
- *     * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- *     * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- *     * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
- *     * `select` **`as`** `label` **`group by`** `group`
- *         **`for` `(`**`key`**`,`** `value`**`) in`** `object`
- *
- * Where:
- *
- *   * `array` / `object`: an expression which evaluates to an array / object to iterate over.
- *   * `value`: local variable which will refer to each item in the `array` or each property value
- *      of `object` during iteration.
- *   * `key`: local variable which will refer to a property name in `object` during iteration.
- *   * `label`: The result of this expression will be the label for `<option>` element. The
- *     `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
- *   * `select`: The result of this expression will be bound to the model of the parent `<select>`
- *      element. If not specified, `select` expression will default to `value`.
- *   * `group`: The result of this expression will be used to group options using the `<optgroup>`
- *      DOM element.
- *   * `trackexpr`: Used when working with an array of objects. The result of this expression will be
- *      used to identify the objects in the array. The `trackexpr` will most likely refer to the
- *     `value` variable (e.g. `value.propertyName`). With this the selection is preserved
- *      even when the options are recreated (e.g. reloaded from the server).
- *
- * @example
-    <example module="selectExample">
-      <file name="index.html">
-        <script>
-        angular.module('selectExample', [])
-          .controller('ExampleController', ['$scope', function($scope) {
-            $scope.colors = [
-              {name:'black', shade:'dark'},
-              {name:'white', shade:'light'},
-              {name:'red', shade:'dark'},
-              {name:'blue', shade:'dark'},
-              {name:'yellow', shade:'light'}
-            ];
-            $scope.myColor = $scope.colors[2]; // red
-          }]);
-        </script>
-        <div ng-controller="ExampleController">
-          <ul>
-            <li ng-repeat="color in colors">
-              Name: <input ng-model="color.name">
-              [<a href ng-click="colors.splice($index, 1)">X</a>]
-            </li>
-            <li>
-              [<a href ng-click="colors.push({})">add</a>]
-            </li>
-          </ul>
-          <hr/>
-          Color (null not allowed):
-          <select ng-model="myColor" ng-options="color.name for color in colors"></select><br>
-
-          Color (null allowed):
-          <span  class="nullable">
-            <select ng-model="myColor" ng-options="color.name for color in colors">
-              <option value="">-- choose color --</option>
-            </select>
-          </span><br/>
-
-          Color grouped by shade:
-          <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
-          </select><br/>
-
-
-          Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br>
-          <hr/>
-          Currently selected: {{ {selected_color:myColor} }}
-          <div style="border:solid 1px black; height:20px"
-               ng-style="{'background-color':myColor.name}">
-          </div>
-        </div>
-      </file>
-      <file name="protractor.js" type="protractor">
-         it('should check ng-options', function() {
-           expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
-           element.all(by.model('myColor')).first().click();
-           element.all(by.css('select[ng-model="myColor"] option')).first().click();
-           expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
-           element(by.css('.nullable select[ng-model="myColor"]')).click();
-           element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
-           expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
-         });
-      </file>
-    </example>
  */
-
-var ngOptionsDirective = valueFn({
-  restrict: 'A',
-  terminal: true
-});
-
-// jshint maxlen: false
-var selectDirective = ['$compile', '$parse', function($compile,   $parse) {
-                         //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
-  var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
-      nullModelCtrl = {$setViewValue: noop};
-// jshint maxlen: 100
+var selectDirective = function() {
+  var lastView;
 
   return {
     restrict: 'E',
     require: ['select', '?ngModel'],
-    controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
-      var self = this,
-          optionsMap = {},
-          ngModelCtrl = nullModelCtrl,
-          nullOption,
-          unknownOption;
-
-
-      self.databound = $attrs.ngModel;
-
-
-      self.init = function(ngModelCtrl_, nullOption_, unknownOption_) {
-        ngModelCtrl = ngModelCtrl_;
-        nullOption = nullOption_;
-        unknownOption = unknownOption_;
-      };
-
-
-      self.addOption = function(value, element) {
-        assertNotHasOwnProperty(value, '"option value"');
-        optionsMap[value] = true;
-
-        if (ngModelCtrl.$viewValue == value) {
-          $element.val(value);
-          if (unknownOption.parent()) unknownOption.remove();
-        }
-        // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
-        // Adding an <option selected="selected"> element to a <select required="required"> should
-        // automatically select the new element
-        if (element && element[0].hasAttribute('selected')) {
-          element[0].selected = true;
-        }
-      };
-
-
-      self.removeOption = function(value) {
-        if (this.hasOption(value)) {
-          delete optionsMap[value];
-          if (ngModelCtrl.$viewValue === value) {
-            this.renderUnknownOption(value);
-          }
-        }
-      };
+    controller: SelectController,
+    link: function(scope, element, attr, ctrls) {
 
+      // if ngModel is not defined, we don't need to do anything
+      var ngModelCtrl = ctrls[1];
+      if (!ngModelCtrl) return;
 
-      self.renderUnknownOption = function(val) {
-        var unknownVal = '? ' + hashKey(val) + ' ?';
-        unknownOption.val(unknownVal);
-        $element.prepend(unknownOption);
-        $element.val(unknownVal);
-        unknownOption.prop('selected', true); // needed for IE
-      };
+      var selectCtrl = ctrls[0];
 
+      selectCtrl.ngModelCtrl = ngModelCtrl;
 
-      self.hasOption = function(value) {
-        return optionsMap.hasOwnProperty(value);
+      // We delegate rendering to the `writeValue` method, which can be changed
+      // if the select can have multiple selected values or if the options are being
+      // generated by `ngOptions`
+      ngModelCtrl.$render = function() {
+        selectCtrl.writeValue(ngModelCtrl.$viewValue);
       };
 
-      $scope.$on('$destroy', function() {
-        // disable unknown option so that we don't do work when the whole select is being destroyed
-        self.renderUnknownOption = noop;
+      // When the selected item(s) changes we delegate getting the value of the select control
+      // to the `readValue` method, which can be changed if the select can have multiple
+      // selected values or if the options are being generated by `ngOptions`
+      element.on('change', function() {
+        scope.$apply(function() {
+          ngModelCtrl.$setViewValue(selectCtrl.readValue());
+        });
       });
-    }],
 
-    link: function(scope, element, attr, ctrls) {
-      // if ngModel is not defined, we don't need to do anything
-      if (!ctrls[1]) return;
-
-      var selectCtrl = ctrls[0],
-          ngModelCtrl = ctrls[1],
-          multiple = attr.multiple,
-          optionsExp = attr.ngOptions,
-          nullOption = false, // if false, user will not be able to select it (used by ngOptions)
-          emptyOption,
-          renderScheduled = false,
-          // we can't just jqLite('<option>') since jqLite is not smart enough
-          // to create it in <select> and IE barfs otherwise.
-          optionTemplate = jqLite(document.createElement('option')),
-          optGroupTemplate =jqLite(document.createElement('optgroup')),
-          unknownOption = optionTemplate.clone();
-
-      // find "null" option
-      for (var i = 0, children = element.children(), ii = children.length; i < ii; i++) {
-        if (children[i].value === '') {
-          emptyOption = nullOption = children.eq(i);
-          break;
-        }
-      }
-
-      selectCtrl.init(ngModelCtrl, nullOption, unknownOption);
-
-      // required validator
-      if (multiple) {
-        ngModelCtrl.$isEmpty = function(value) {
-          return !value || value.length === 0;
-        };
-      }
-
-      if (optionsExp) setupAsOptions(scope, element, ngModelCtrl);
-      else if (multiple) setupAsMultiple(scope, element, ngModelCtrl);
-      else setupAsSingle(scope, element, ngModelCtrl, selectCtrl);
-
-
-      ////////////////////////////
-
-
-
-      function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) {
-        ngModelCtrl.$render = function() {
-          var viewValue = ngModelCtrl.$viewValue;
-
-          if (selectCtrl.hasOption(viewValue)) {
-            if (unknownOption.parent()) unknownOption.remove();
-            selectElement.val(viewValue);
-            if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy
-          } else {
-            if (isUndefined(viewValue) && emptyOption) {
-              selectElement.val('');
-            } else {
-              selectCtrl.renderUnknownOption(viewValue);
+      // If the select allows multiple values then we need to modify how we read and write
+      // values from and to the control; also what it means for the value to be empty and
+      // we have to add an extra watch since ngModel doesn't work well with arrays - it
+      // doesn't trigger rendering if only an item in the array changes.
+      if (attr.multiple) {
+
+        // Read value now needs to check each option to see if it is selected
+        selectCtrl.readValue = function readMultipleValue() {
+          var array = [];
+          forEach(element.find('option'), function(option) {
+            if (option.selected) {
+              array.push(option.value);
             }
-          }
-        };
-
-        selectElement.on('change', function() {
-          scope.$apply(function() {
-            if (unknownOption.parent()) unknownOption.remove();
-            ngModelCtrl.$setViewValue(selectElement.val());
           });
-        });
-      }
+          return array;
+        };
 
-      function setupAsMultiple(scope, selectElement, ctrl) {
-        var lastView;
-        ctrl.$render = function() {
-          var items = new HashMap(ctrl.$viewValue);
-          forEach(selectElement.find('option'), function(option) {
+        // Write value now needs to set the selected property of each matching option
+        selectCtrl.writeValue = function writeMultipleValue(value) {
+          var items = new HashMap(value);
+          forEach(element.find('option'), function(option) {
             option.selected = isDefined(items.get(option.value));
           });
         };
@@ -344,400 +200,45 @@ var selectDirective = ['$compile', '$parse', function($compile,   $parse) {
         // we have to do it on each watch since ngModel watches reference, but
         // we need to work of an array, so we need to see if anything was inserted/removed
         scope.$watch(function selectMultipleWatch() {
-          if (!equals(lastView, ctrl.$viewValue)) {
-            lastView = shallowCopy(ctrl.$viewValue);
-            ctrl.$render();
+          if (!equals(lastView, ngModelCtrl.$viewValue)) {
+            lastView = shallowCopy(ngModelCtrl.$viewValue);
+            ngModelCtrl.$render();
           }
         });
 
-        selectElement.on('change', function() {
-          scope.$apply(function() {
-            var array = [];
-            forEach(selectElement.find('option'), function(option) {
-              if (option.selected) {
-                array.push(option.value);
-              }
-            });
-            ctrl.$setViewValue(array);
-          });
-        });
-      }
-
-      function setupAsOptions(scope, selectElement, ctrl) {
-        var match;
-
-        if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) {
-          throw ngOptionsMinErr('iexp',
-            "Expected expression in form of " +
-            "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
-            " but got '{0}'. Element: {1}",
-            optionsExp, startingTag(selectElement));
-        }
-
-        var displayFn = $parse(match[2] || match[1]),
-            valueName = match[4] || match[6],
-            selectAs = / as /.test(match[0]) && match[1],
-            selectAsFn = selectAs ? $parse(selectAs) : null,
-            keyName = match[5],
-            groupByFn = $parse(match[3] || ''),
-            valueFn = $parse(match[2] ? match[1] : valueName),
-            valuesFn = $parse(match[7]),
-            track = match[8],
-            trackFn = track ? $parse(match[8]) : null,
-            trackKeysCache = {},
-            // This is an array of array of existing option groups in DOM.
-            // We try to reuse these if possible
-            // - optionGroupsCache[0] is the options with no option group
-            // - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
-            optionGroupsCache = [[{element: selectElement, label:''}]],
-            //re-usable object to represent option's locals
-            locals = {};
-
-        if (nullOption) {
-          // compile the element since there might be bindings in it
-          $compile(nullOption)(scope);
-
-          // remove the class, which is added automatically because we recompile the element and it
-          // becomes the compilation root
-          nullOption.removeClass('ng-scope');
-
-          // we need to remove it before calling selectElement.empty() because otherwise IE will
-          // remove the label from the element. wtf?
-          nullOption.remove();
-        }
-
-        // clear contents, we'll add what's needed based on the model
-        selectElement.empty();
-
-        selectElement.on('change', selectionChanged);
-
-        ctrl.$render = render;
-
-        scope.$watchCollection(valuesFn, scheduleRendering);
-        scope.$watchCollection(getLabels, scheduleRendering);
-
-        if (multiple) {
-          scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering);
-        }
-
-        // ------------------------------------------------------------------ //
-
-        function callExpression(exprFn, key, value) {
-          locals[valueName] = value;
-          if (keyName) locals[keyName] = key;
-          return exprFn(scope, locals);
-        }
-
-        function selectionChanged() {
-          scope.$apply(function() {
-            var collection = valuesFn(scope) || [];
-            var viewValue;
-            if (multiple) {
-              viewValue = [];
-              forEach(selectElement.val(), function(selectedKey) {
-                  selectedKey = trackFn ? trackKeysCache[selectedKey] : selectedKey;
-                viewValue.push(getViewValue(selectedKey, collection[selectedKey]));
-              });
-            } else {
-              var selectedKey = trackFn ? trackKeysCache[selectElement.val()] : selectElement.val();
-              viewValue = getViewValue(selectedKey, collection[selectedKey]);
-            }
-            ctrl.$setViewValue(viewValue);
-            render();
-          });
-        }
-
-        function getViewValue(key, value) {
-          if (key === '?') {
-            return undefined;
-          } else if (key === '') {
-            return null;
-          } else {
-            var viewValueFn = selectAsFn ? selectAsFn : valueFn;
-            return callExpression(viewValueFn, key, value);
-          }
-        }
-
-        function getLabels() {
-          var values = valuesFn(scope);
-          var toDisplay;
-          if (values && isArray(values)) {
-            toDisplay = new Array(values.length);
-            for (var i = 0, ii = values.length; i < ii; i++) {
-              toDisplay[i] = callExpression(displayFn, i, values[i]);
-            }
-            return toDisplay;
-          } else if (values) {
-            // TODO: Add a test for this case
-            toDisplay = {};
-            for (var prop in values) {
-              if (values.hasOwnProperty(prop)) {
-                toDisplay[prop] = callExpression(displayFn, prop, values[prop]);
-              }
-            }
-          }
-          return toDisplay;
-        }
-
-        function createIsSelectedFn(viewValue) {
-          var selectedSet;
-          if (multiple) {
-            if (trackFn && isArray(viewValue)) {
-
-              selectedSet = new HashMap([]);
-              for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) {
-                // tracking by key
-                selectedSet.put(callExpression(trackFn, null, viewValue[trackIndex]), true);
-              }
-            } else {
-              selectedSet = new HashMap(viewValue);
-            }
-          } else if (trackFn) {
-            viewValue = callExpression(trackFn, null, viewValue);
-          }
-
-          return function isSelected(key, value) {
-            var compareValueFn;
-            if (trackFn) {
-              compareValueFn = trackFn;
-            } else if (selectAsFn) {
-              compareValueFn = selectAsFn;
-            } else {
-              compareValueFn = valueFn;
-            }
-
-            if (multiple) {
-              return isDefined(selectedSet.remove(callExpression(compareValueFn, key, value)));
-            } else {
-              return viewValue === callExpression(compareValueFn, key, value);
-            }
-          };
-        }
-
-        function scheduleRendering() {
-          if (!renderScheduled) {
-            scope.$$postDigest(render);
-            renderScheduled = true;
-          }
-        }
-
-        /**
-         * A new labelMap is created with each render.
-         * This function is called for each existing option with added=false,
-         * and each new option with added=true.
-         * - Labels that are passed to this method twice,
-         * (once with added=true and once with added=false) will end up with a value of 0, and
-         * will cause no change to happen to the corresponding option.
-         * - Labels that are passed to this method only once with added=false will end up with a
-         * value of -1 and will eventually be passed to selectCtrl.removeOption()
-         * - Labels that are passed to this method only once with added=true will end up with a
-         * value of 1 and will eventually be passed to selectCtrl.addOption()
-        */
-        function updateLabelMap(labelMap, label, added) {
-          labelMap[label] = labelMap[label] || 0;
-          labelMap[label] += (added ? 1 : -1);
-        }
-
-        function render() {
-          renderScheduled = false;
-
-          // Temporary location for the option groups before we render them
-          var optionGroups = {'':[]},
-              optionGroupNames = [''],
-              optionGroupName,
-              optionGroup,
-              option,
-              existingParent, existingOptions, existingOption,
-              viewValue = ctrl.$viewValue,
-              values = valuesFn(scope) || [],
-              keys = keyName ? sortedKeys(values) : values,
-              key,
-              value,
-              groupLength, length,
-              groupIndex, index,
-              labelMap = {},
-              selected,
-              isSelected = createIsSelectedFn(viewValue),
-              anySelected = false,
-              lastElement,
-              element,
-              label,
-              optionId;
-
-          trackKeysCache = {};
-
-          // We now build up the list of options we need (we merge later)
-          for (index = 0; length = keys.length, index < length; index++) {
-            key = index;
-            if (keyName) {
-              key = keys[index];
-              if (key.charAt(0) === '$') continue;
-            }
-            value = values[key];
-
-            optionGroupName = callExpression(groupByFn, key, value) || '';
-            if (!(optionGroup = optionGroups[optionGroupName])) {
-              optionGroup = optionGroups[optionGroupName] = [];
-              optionGroupNames.push(optionGroupName);
-            }
-
-            selected = isSelected(key, value);
-            anySelected = anySelected || selected;
-
-            label = callExpression(displayFn, key, value); // what will be seen by the user
-
-            // doing displayFn(scope, locals) || '' overwrites zero values
-            label = isDefined(label) ? label : '';
-            optionId = trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index);
-            if (trackFn) {
-              trackKeysCache[optionId] = key;
-            }
-
-            optionGroup.push({
-              // either the index into array or key from object
-              id: optionId,
-              label: label,
-              selected: selected                   // determine if we should be selected
-            });
-          }
-          if (!multiple) {
-            if (nullOption || viewValue === null) {
-              // insert null option if we have a placeholder, or the model is null
-              optionGroups[''].unshift({id:'', label:'', selected:!anySelected});
-            } else if (!anySelected) {
-              // option could not be found, we have to insert the undefined item
-              optionGroups[''].unshift({id:'?', label:'', selected:true});
-            }
-          }
-
-          // Now we need to update the list of DOM nodes to match the optionGroups we computed above
-          for (groupIndex = 0, groupLength = optionGroupNames.length;
-               groupIndex < groupLength;
-               groupIndex++) {
-            // current option group name or '' if no group
-            optionGroupName = optionGroupNames[groupIndex];
-
-            // list of options for that group. (first item has the parent)
-            optionGroup = optionGroups[optionGroupName];
-
-            if (optionGroupsCache.length <= groupIndex) {
-              // we need to grow the optionGroups
-              existingParent = {
-                element: optGroupTemplate.clone().attr('label', optionGroupName),
-                label: optionGroup.label
-              };
-              existingOptions = [existingParent];
-              optionGroupsCache.push(existingOptions);
-              selectElement.append(existingParent.element);
-            } else {
-              existingOptions = optionGroupsCache[groupIndex];
-              existingParent = existingOptions[0];  // either SELECT (no group) or OPTGROUP element
-
-              // update the OPTGROUP label if not the same.
-              if (existingParent.label != optionGroupName) {
-                existingParent.element.attr('label', existingParent.label = optionGroupName);
-              }
-            }
+        // If we are a multiple select then value is now a collection
+        // so the meaning of $isEmpty changes
+        ngModelCtrl.$isEmpty = function(value) {
+          return !value || value.length === 0;
+        };
 
-            lastElement = null;  // start at the beginning
-            for (index = 0, length = optionGroup.length; index < length; index++) {
-              option = optionGroup[index];
-              if ((existingOption = existingOptions[index + 1])) {
-                // reuse elements
-                lastElement = existingOption.element;
-                if (existingOption.label !== option.label) {
-                  updateLabelMap(labelMap, existingOption.label, false);
-                  updateLabelMap(labelMap, option.label, true);
-                  lastElement.text(existingOption.label = option.label);
-                  lastElement.prop('label', existingOption.label);
-                }
-                if (existingOption.id !== option.id) {
-                  lastElement.val(existingOption.id = option.id);
-                }
-                // lastElement.prop('selected') provided by jQuery has side-effects
-                if (lastElement[0].selected !== option.selected) {
-                  lastElement.prop('selected', (existingOption.selected = option.selected));
-                  if (msie) {
-                    // See #7692
-                    // The selected item wouldn't visually update on IE without this.
-                    // Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well
-                    lastElement.prop('selected', existingOption.selected);
-                  }
-                }
-              } else {
-                // grow elements
-
-                // if it's a null option
-                if (option.id === '' && nullOption) {
-                  // put back the pre-compiled element
-                  element = nullOption;
-                } else {
-                  // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
-                  // in this version of jQuery on some browser the .text() returns a string
-                  // rather then the element.
-                  (element = optionTemplate.clone())
-                      .val(option.id)
-                      .prop('selected', option.selected)
-                      .attr('selected', option.selected)
-                      .prop('label', option.label)
-                      .text(option.label);
-                }
-
-                existingOptions.push(existingOption = {
-                    element: element,
-                    label: option.label,
-                    id: option.id,
-                    selected: option.selected
-                });
-                updateLabelMap(labelMap, option.label, true);
-                if (lastElement) {
-                  lastElement.after(element);
-                } else {
-                  existingParent.element.append(element);
-                }
-                lastElement = element;
-              }
-            }
-            // remove any excessive OPTIONs in a group
-            index++; // increment since the existingOptions[0] is parent element not OPTION
-            while (existingOptions.length > index) {
-              option = existingOptions.pop();
-              updateLabelMap(labelMap, option.label, false);
-              option.element.remove();
-            }
-          }
-          // remove any excessive OPTGROUPs from select
-          while (optionGroupsCache.length > groupIndex) {
-            // remove all the labels in the option group
-            optionGroup = optionGroupsCache.pop();
-            for (index = 1; index < optionGroup.length; ++index) {
-              updateLabelMap(labelMap, optionGroup[index].label, false);
-            }
-            optionGroup[0].element.remove();
-          }
-          forEach(labelMap, function(count, label) {
-            if (count > 0) {
-              selectCtrl.addOption(label);
-            } else if (count < 0) {
-              selectCtrl.removeOption(label);
-            }
-          });
-        }
       }
     }
   };
-}];
+};
+
 
+// The option directive is purely designed to communicate the existence (or lack of)
+// of dynamically created (and destroyed) option elements to their containing select
+// directive via its controller.
 var optionDirective = ['$interpolate', function($interpolate) {
-  var nullSelectCtrl = {
-    addOption: noop,
-    removeOption: noop
-  };
+
+  function chromeHack(optionElement) {
+    // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
+    // Adding an <option selected="selected"> element to a <select required="required"> should
+    // automatically select the new element
+    if (optionElement[0].hasAttribute('selected')) {
+      optionElement[0].selected = true;
+    }
+  }
 
   return {
     restrict: 'E',
     priority: 100,
     compile: function(element, attr) {
+
+      // If the value attribute is not defined then we fall back to the
+      // text content of the option element, which may be interpolated
       if (isUndefined(attr.value)) {
         var interpolateFn = $interpolate(element.text(), true);
         if (!interpolateFn) {
@@ -746,30 +247,39 @@ var optionDirective = ['$interpolate', function($interpolate) {
       }
 
       return function(scope, element, attr) {
+
+        // This is an optimization over using ^^ since we don't want to have to search
+        // all the way to the root of the DOM for every single option element
         var selectCtrlName = '$selectController',
             parent = element.parent(),
             selectCtrl = parent.data(selectCtrlName) ||
               parent.parent().data(selectCtrlName); // in case we are in optgroup
 
-        if (!selectCtrl || !selectCtrl.databound) {
-          selectCtrl = nullSelectCtrl;
-        }
+        // Only update trigger option updates if this is an option within a `select`
+        // that also has `ngModel` attached
+        if (selectCtrl && selectCtrl.ngModelCtrl) {
 
-        if (interpolateFn) {
-          scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
-            attr.$set('value', newVal);
-            if (oldVal !== newVal) {
-              selectCtrl.removeOption(oldVal);
-            }
-            selectCtrl.addOption(newVal, element);
+          if (interpolateFn) {
+            scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
+              attr.$set('value', newVal);
+              if (oldVal !== newVal) {
+                selectCtrl.removeOption(oldVal);
+              }
+              selectCtrl.addOption(newVal, element);
+              selectCtrl.ngModelCtrl.$render();
+              chromeHack(element);
+            });
+          } else {
+            selectCtrl.addOption(attr.value, element);
+            selectCtrl.ngModelCtrl.$render();
+            chromeHack(element);
+          }
+
+          element.on('$destroy', function() {
+            selectCtrl.removeOption(attr.value);
+            selectCtrl.ngModelCtrl.$render();
           });
-        } else {
-          selectCtrl.addOption(attr.value, element);
         }
-
-        element.on('$destroy', function() {
-          selectCtrl.removeOption(attr.value);
-        });
       };
     }
   };
diff --git a/test/ng/directive/ngOptionsSpec.js b/test/ng/directive/ngOptionsSpec.js
new file mode 100644
index 000000000000..21f890fc5406
--- /dev/null
+++ b/test/ng/directive/ngOptionsSpec.js
@@ -0,0 +1,1914 @@
+'use strict';
+
+describe('ngOptions', function() {
+
+  var scope, formElement, element, $compile;
+
+  function compile(html) {
+    formElement = jqLite('<form name="form">' + html + '</form>');
+    element = formElement.find('select');
+    $compile(formElement)(scope);
+    scope.$apply();
+  }
+
+  function setSelectValue(selectElement, optionIndex) {
+    var option = selectElement.find('option').eq(optionIndex);
+    selectElement.val(option.val());
+    browserTrigger(element, 'change');
+  }
+
+
+  beforeEach(function() {
+    this.addMatchers({
+      toEqualSelectValue: function(value, multiple) {
+        var errors = [];
+        var actual = this.actual.val();
+
+        if (multiple) {
+          value = value.map(function(val) { return hashKey(val); });
+          actual = actual || [];
+        } else {
+          value = hashKey(value);
+        }
+
+        if (!equals(actual, value)) {
+          errors.push('Expected select value "' + actual + '" to equal "' + value + '"');
+        }
+        this.message = function() {
+          return errors.join('\n');
+        };
+
+        return errors.length === 0;
+      },
+      toEqualOption: function(value, text, label) {
+        var errors = [];
+        var hash = hashKey(value);
+        if (this.actual.attr('value') !== hash) {
+          errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "' + hash + '"');
+        }
+        if (text && this.actual.text() !== text) {
+          errors.push('Expected option text "' + this.actual.text() + '" to equal "' + text + '"');
+        }
+        if (label && this.actual.attr('label') !== label) {
+          errors.push('Expected option label "' + this.actual.attr('label') + '" to equal "' + label + '"');
+        }
+
+        this.message = function() {
+          return errors.join('\n');
+        };
+
+        return errors.length === 0;
+      },
+      toEqualTrackedOption: function(value, text, label) {
+        var errors = [];
+        if (this.actual.attr('value') !== '' + value) {
+          errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "' + value + '"');
+        }
+        if (text && this.actual.text() !== text) {
+          errors.push('Expected option text "' + this.actual.text() + '" to equal "' + text + '"');
+        }
+        if (label && this.actual.attr('label') !== label) {
+          errors.push('Expected option label "' + this.actual.attr('label') + '" to equal "' + label + '"');
+        }
+
+        this.message = function() {
+          return errors.join('\n');
+        };
+
+        return errors.length === 0;
+      },
+      toEqualUnknownOption: function() {
+        var errors = [];
+        if (this.actual.attr('value') !== '?') {
+          errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "?"');
+        }
+
+        this.message = function() {
+          return errors.join('\n');
+        };
+
+        return errors.length === 0;
+      },
+      toEqualUnknownValue: function(value) {
+        var errors = [];
+        if (this.actual !== '?') {
+          errors.push('Expected select value "' + this.actual + '" to equal "?"');
+        }
+
+        this.message = function() {
+          return errors.join('\n');
+        };
+
+        return errors.length === 0;
+      }
+    });
+  });
+
+  beforeEach(inject(function($rootScope, _$compile_) {
+    scope = $rootScope.$new(); //create a child scope because the root scope can't be $destroy-ed
+    $compile = _$compile_;
+    formElement = element = null;
+  }));
+
+
+  afterEach(function() {
+    scope.$destroy(); //disables unknown option work during destruction
+    dealoc(formElement);
+  });
+
+  function createSelect(attrs, blank, unknown) {
+    var html = '<select';
+    forEach(attrs, function(value, key) {
+      if (isBoolean(value)) {
+        if (value) html += ' ' + key;
+      } else {
+        html += ' ' + key + '="' + value + '"';
+      }
+    });
+    html += '>' +
+      (blank ? (isString(blank) ? blank : '<option value="">blank</option>') : '') +
+      (unknown ? (isString(unknown) ? unknown : '<option value="?">unknown</option>') : '') +
+    '</select>';
+
+    compile(html);
+  }
+
+  function createSingleSelect(blank, unknown) {
+    createSelect({
+      'ng-model':'selected',
+      'ng-options':'value.name for value in values'
+    }, blank, unknown);
+  }
+
+  function createMultiSelect(blank, unknown) {
+    createSelect({
+      'ng-model':'selected',
+      'multiple':true,
+      'ng-options':'value.name for value in values'
+    }, blank, unknown);
+  }
+
+
+  it('should throw when not formated "? for ? in ?"', function() {
+    expect(function() {
+        compile('<select ng-model="selected" ng-options="i dont parse"></select>')(scope);
+      }).toThrowMinErr('ngOptions', 'iexp', /Expected expression in form of/);
+  });
+
+
+  it('should render a list', function() {
+    createSingleSelect();
+
+    scope.$apply(function() {
+      scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+      scope.selected = scope.values[1];
+    });
+
+    var options = element.find('option');
+    expect(options.length).toEqual(3);
+    expect(options.eq(0)).toEqualOption(scope.values[0], 'A');
+    expect(options.eq(1)).toEqualOption(scope.values[1], 'B');
+    expect(options.eq(2)).toEqualOption(scope.values[2], 'C');
+    expect(options[1].selected).toEqual(true);
+  });
+
+
+  it('should render an object', function() {
+    createSelect({
+      'ng-model': 'selected',
+      'ng-options': 'value as key for (key, value) in object'
+    });
+
+    scope.$apply(function() {
+      scope.object = {'red': 'FF0000', 'green': '00FF00', 'blue': '0000FF'};
+      scope.selected = scope.object.green;
+    });
+
+    var options = element.find('option');
+    expect(options.length).toEqual(3);
+    expect(options.eq(0)).toEqualOption('FF0000', 'red');
+    expect(options.eq(1)).toEqualOption('00FF00', 'green');
+    expect(options.eq(2)).toEqualOption('0000FF', 'blue');
+    expect(options[1].selected).toEqual(true);
+
+    scope.$apply('object.azur = "8888FF"');
+
+    options = element.find('option');
+    expect(options[1].selected).toEqual(true);
+
+    scope.$apply('selected = object.azur');
+
+    options = element.find('option');
+    expect(options[3].selected).toEqual(true);
+
+  });
+
+
+  it('should render zero as a valid display value', function() {
+    createSingleSelect();
+
+    scope.$apply(function() {
+      scope.values = [{name: 0}, {name: 1}, {name: 2}];
+      scope.selected = scope.values[0];
+    });
+
+    var options = element.find('option');
+    expect(options.length).toEqual(3);
+    expect(options.eq(0)).toEqualOption(scope.values[0], '0');
+    expect(options.eq(1)).toEqualOption(scope.values[1], '1');
+    expect(options.eq(2)).toEqualOption(scope.values[2], '2');
+  });
+
+
+  it('should not be set when an option is selected and options are set asynchronously',
+    inject(function($timeout) {
+      compile('<select ng-model="model" ng-options="opt.id as opt.label for opt in options">' +
+                  '</select>');
+
+      scope.$apply(function() {
+        scope.model = 0;
+      });
+
+      $timeout(function() {
+        scope.options = [
+          {id: 0, label: 'x'},
+          {id: 1, label: 'y'}
+        ];
+      }, 0);
+
+      $timeout.flush();
+
+      var options = element.find('option');
+
+      expect(options.length).toEqual(2);
+      expect(options.eq(0)).toEqualOption(0, 'x');
+      expect(options.eq(1)).toEqualOption(1, 'y');
+    })
+  );
+
+
+  it('should grow list', function() {
+    createSingleSelect();
+
+    scope.$apply(function() {
+      scope.values = [];
+    });
+
+    expect(element.find('option').length).toEqual(1); // because we add special unknown option
+    expect(element.find('option').eq(0)).toEqualUnknownOption();
+
+    scope.$apply(function() {
+      scope.values.push({name:'A'});
+      scope.selected = scope.values[0];
+    });
+
+    expect(element.find('option').length).toEqual(1);
+    expect(element.find('option')).toEqualOption(scope.values[0], 'A');
+
+    scope.$apply(function() {
+      scope.values.push({name:'B'});
+    });
+
+    expect(element.find('option').length).toEqual(2);
+    expect(element.find('option').eq(0)).toEqualOption(scope.values[0], 'A');
+    expect(element.find('option').eq(1)).toEqualOption(scope.values[1], 'B');
+  });
+
+
+  it('should shrink list', function() {
+    createSingleSelect();
+
+    scope.$apply(function() {
+      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+      scope.selected = scope.values[0];
+    });
+
+    expect(element.find('option').length).toEqual(3);
+
+    scope.$apply(function() {
+      scope.values.pop();
+    });
+
+    expect(element.find('option').length).toEqual(2);
+    expect(element.find('option').eq(0)).toEqualOption(scope.values[0], 'A');
+    expect(element.find('option').eq(1)).toEqualOption(scope.values[1], 'B');
+
+    scope.$apply(function() {
+      scope.values.pop();
+    });
+
+    expect(element.find('option').length).toEqual(1);
+    expect(element.find('option')).toEqualOption(scope.values[0], 'A');
+
+    scope.$apply(function() {
+      scope.values.pop();
+      scope.selected = null;
+    });
+
+    expect(element.find('option').length).toEqual(1); // we add back the special empty option
+  });
+
+
+  it('should shrink and then grow list', function() {
+    createSingleSelect();
+
+    scope.$apply(function() {
+      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+      scope.selected = scope.values[0];
+    });
+
+    expect(element.find('option').length).toEqual(3);
+
+    scope.$apply(function() {
+      scope.values = [{name: '1'}, {name: '2'}];
+      scope.selected = scope.values[0];
+    });
+
+    expect(element.find('option').length).toEqual(2);
+
+    scope.$apply(function() {
+      scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+      scope.selected = scope.values[0];
+    });
+
+    expect(element.find('option').length).toEqual(3);
+  });
+
+
+  it('should update list', function() {
+    createSingleSelect();
+
+    scope.$apply(function() {
+      scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+      scope.selected = scope.values[0];
+    });
+
+    scope.$apply(function() {
+      scope.values = [{name: 'B'}, {name: 'C'}, {name: 'D'}];
+      scope.selected = scope.values[0];
+    });
+
+    var options = element.find('option');
+    expect(options.length).toEqual(3);
+    expect(options.eq(0)).toEqualOption(scope.values[0], 'B');
+    expect(options.eq(1)).toEqualOption(scope.values[1], 'C');
+    expect(options.eq(2)).toEqualOption(scope.values[2], 'D');
+  });
+
+  it('should preserve pre-existing empty option', function() {
+    createSingleSelect(true);
+
+    scope.$apply(function() {
+      scope.values = [];
+    });
+    expect(element.find('option').length).toEqual(1);
+
+    scope.$apply(function() {
+      scope.values = [{name: 'A'}];
+      scope.selected = scope.values[0];
+    });
+
+    expect(element.find('option').length).toEqual(2);
+    expect(jqLite(element.find('option')[0]).text()).toEqual('blank');
+    expect(jqLite(element.find('option')[1]).text()).toEqual('A');
+
+    scope.$apply(function() {
+      scope.values = [];
+      scope.selected = null;
+    });
+
+    expect(element.find('option').length).toEqual(1);
+    expect(jqLite(element.find('option')[0]).text()).toEqual('blank');
+  });
+
+
+  it('should ignore $ and $$ properties', function() {
+    createSelect({
+      'ng-options': 'key as value for (key, value) in object',
+      'ng-model': 'selected'
+    });
+
+    scope.$apply(function() {
+      scope.object = {'regularProperty': 'visible', '$$private': 'invisible', '$property': 'invisible'};
+      scope.selected = 'regularProperty';
+    });
+
+    var options = element.find('option');
+    expect(options.length).toEqual(1);
+    expect(options.eq(0)).toEqualOption('regularProperty', 'visible');
+  });
+
+
+  it('should allow expressions over multiple lines', function() {
+    scope.isNotFoo = function(item) {
+      return item.name !== 'Foo';
+    };
+
+    createSelect({
+      'ng-options': 'key.id\n' +
+        'for key in values\n' +
+        '| filter:isNotFoo',
+      'ng-model': 'selected'
+    });
+
+    scope.$apply(function() {
+      scope.values = [{'id': 1, 'name': 'Foo'},
+                      {'id': 2, 'name': 'Bar'},
+                      {'id': 3, 'name': 'Baz'}];
+      scope.selected = scope.values[0];
+    });
+
+    var options = element.find('option');
+    expect(options.length).toEqual(3);
+    expect(options.eq(1)).toEqualOption(scope.values[1], '2');
+    expect(options.eq(2)).toEqualOption(scope.values[2], '3');
+  });
+
+
+  it('should not update selected property of an option element on digest with no change event',
+      function() {
+    // ng-options="value.name for value in values"
+    // ng-model="selected"
+    createSingleSelect();
+
+    scope.$apply(function() {
+      scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+      scope.selected = scope.values[0];
+    });
+
+    var options = element.find('option');
+
+    expect(scope.selected).toEqual(jasmine.objectContaining({ name: 'A' }));
+    expect(options.eq(0).prop('selected')).toBe(true);
+    expect(options.eq(1).prop('selected')).toBe(false);
+
+    var optionToSelect = options.eq(1);
+
+    expect(optionToSelect.text()).toBe('B');
+
+    optionToSelect.prop('selected', true);
+    scope.$digest();
+
+    expect(optionToSelect.prop('selected')).toBe(true);
+    expect(scope.selected).toBe(scope.values[0]);
+  });
+
+
+  // bug fix #9621
+  it('should update the label property', function() {
+    // ng-options="value.name for value in values"
+    // ng-model="selected"
+    createSingleSelect();
+
+    scope.$apply(function() {
+      scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+      scope.selected = scope.values[0];
+    });
+
+    var options = element.find('option');
+    expect(options.eq(0).prop('label')).toEqual('A');
+    expect(options.eq(1).prop('label')).toEqual('B');
+    expect(options.eq(2).prop('label')).toEqual('C');
+  });
+
+
+  // bug fix #9714
+  it('should select the matching option when the options are updated', function() {
+
+    // first set up a select with no options
+    scope.selected = '';
+    createSelect({
+      'ng-options': 'val.id as val.label for val in values',
+      'ng-model': 'selected'
+    });
+    var options = element.find('option');
+    // we expect the selected option to be the "unknown" option
+    expect(options.eq(0)).toEqualUnknownOption('');
+    expect(options.eq(0).prop('selected')).toEqual(true);
+
+    // now add some real options - one of which matches the selected value
+    scope.$apply('values = [{id:"",label:"A"},{id:"1",label:"B"},{id:"2",label:"C"}]');
+
+    // we expect the selected option to be the one that matches the correct item
+    // and for the unknown option to have been removed
+    options = element.find('option');
+    expect(element).toEqualSelectValue('');
+    expect(options.eq(0)).toEqualOption('','A');
+  });
+
+
+
+  it('should be possible to use one-time binding on the expression', function() {
+    createSelect({
+      'ng-model': 'someModel',
+      'ng-options': 'o as o for o in ::arr'
+    });
+
+    var options;
+
+    // Initially the options list is just the unknown option
+    options = element.find('option');
+    expect(options.length).toEqual(1);
+
+    // Now initialize the scope and the options should be updated
+    scope.$apply(function() {
+      scope.arr = ['a','b','c'];
+    });
+    options = element.find('option');
+    expect(options.length).toEqual(4);
+    expect(options.eq(0)).toEqualUnknownOption();
+    expect(options.eq(1)).toEqualOption('a');
+    expect(options.eq(2)).toEqualOption('b');
+    expect(options.eq(3)).toEqualOption('c');
+
+    // Change the scope but the options should not change
+    scope.arr = ['w', 'x', 'y', 'z'];
+    scope.$digest();
+    options = element.find('option');
+    expect(options.length).toEqual(4);
+    expect(options.eq(0)).toEqualUnknownOption();
+    expect(options.eq(1)).toEqualOption('a');
+    expect(options.eq(2)).toEqualOption('b');
+    expect(options.eq(3)).toEqualOption('c');
+  });
+
+
+  describe('selectAs expression', function() {
+    beforeEach(function() {
+      scope.arr = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}];
+      scope.obj = {'10': {score: 10, label: 'ten'}, '20': {score: 20, label: 'twenty'}};
+    });
+
+    it('should support single select with array source', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.id as item.label for item in arr'
+      });
+
+      scope.$apply(function() {
+        scope.selected = 10;
+      });
+      expect(element).toEqualSelectValue(10);
+
+      setSelectValue(element, 1);
+      expect(scope.selected).toBe(20);
+    });
+
+
+    it('should support multi select with array source', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'multiple': true,
+        'ng-options': 'item.id as item.label for item in arr'
+      });
+
+      scope.$apply(function() {
+        scope.selected = [10,20];
+      });
+      expect(element).toEqualSelectValue([10,20], true);
+      expect(scope.selected).toEqual([10,20]);
+
+      element.children()[0].selected = false;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual([20]);
+      expect(element).toEqualSelectValue([20], true);
+    });
+
+
+    it('should support single select with object source', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'val.score as val.label for (key, val) in obj'
+      });
+
+      scope.$apply(function() {
+        scope.selected = 10;
+      });
+      expect(element).toEqualSelectValue(10);
+
+      setSelectValue(element, 1);
+      expect(scope.selected).toBe(20);
+    });
+
+
+    it('should support multi select with object source', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'multiple': true,
+        'ng-options': 'val.score as val.label for (key, val) in obj'
+      });
+
+      scope.$apply(function() {
+        scope.selected = [10,20];
+      });
+      expect(element).toEqualSelectValue([10,20], true);
+
+      element.children()[0].selected = false;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual([20]);
+      expect(element).toEqualSelectValue([20], true);
+    });
+  });
+
+
+  describe('trackBy expression', function() {
+    beforeEach(function() {
+      scope.arr = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}];
+      scope.obj = {'1': {score: 10, label: 'ten'}, '2': {score: 20, label: 'twenty'}};
+    });
+
+
+    it('should set the result of track by expression to element value', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.label for item in arr track by item.id'
+      });
+
+      expect(element.val()).toEqualUnknownValue();
+
+      scope.$apply(function() {
+        scope.selected = scope.arr[0];
+      });
+      expect(element.val()).toBe('10');
+
+      scope.$apply(function() {
+        scope.arr[0] = {id: 10, label: 'new ten'};
+      });
+      expect(element.val()).toBe('10');
+
+      element.children()[1].selected = 'selected';
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual(scope.arr[1]);
+    });
+
+
+    it('should use the tracked expression as option value', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.label for item in arr track by item.id'
+      });
+
+      var options = element.find('option');
+      expect(options.length).toEqual(3);
+      expect(options.eq(0)).toEqualUnknownOption();
+      expect(options.eq(1)).toEqualTrackedOption(10, 'ten');
+      expect(options.eq(2)).toEqualTrackedOption(20, 'twenty');
+    });
+
+    it('should preserve value even when reference has changed (single&array)', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.label for item in arr track by item.id'
+      });
+
+      scope.$apply(function() {
+        scope.selected = scope.arr[0];
+      });
+      expect(element.val()).toBe('10');
+
+      scope.$apply(function() {
+        scope.arr[0] = {id: 10, label: 'new ten'};
+      });
+      expect(element.val()).toBe('10');
+
+      element.children()[1].selected = 1;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual(scope.arr[1]);
+    });
+
+
+    it('should preserve value even when reference has changed (multi&array)', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'multiple': true,
+        'ng-options': 'item.label for item in arr track by item.id'
+      });
+
+      scope.$apply(function() {
+        scope.selected = scope.arr;
+      });
+      expect(element.val()).toEqual(['10','20']);
+
+      scope.$apply(function() {
+        scope.arr[0] = {id: 10, label: 'new ten'};
+      });
+      expect(element.val()).toEqual(['10','20']);
+
+      element.children()[0].selected = false;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual([scope.arr[1]]);
+    });
+
+
+    it('should preserve value even when reference has changed (single&object)', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'val.label for (key, val) in obj track by val.score'
+      });
+
+      scope.$apply(function() {
+        scope.selected = scope.obj['1'];
+      });
+      expect(element.val()).toBe('10');
+
+      scope.$apply(function() {
+        scope.obj['1'] = {score: 10, label: 'ten'};
+      });
+      expect(element.val()).toBe('10');
+
+      setSelectValue(element, 1);
+      expect(scope.selected).toBe(scope.obj['2']);
+    });
+
+
+    it('should preserve value even when reference has changed (multi&object)', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'multiple': true,
+        'ng-options': 'val.label for (key, val) in obj track by val.score'
+      });
+
+      scope.$apply(function() {
+        scope.selected = [scope.obj['1']];
+      });
+      expect(element.val()).toEqual(['10']);
+
+      scope.$apply(function() {
+        scope.obj['1'] = {score: 10, label: 'ten'};
+      });
+      expect(element.val()).toEqual(['10']);
+
+      element.children()[1].selected = 'selected';
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual([scope.obj['1'], scope.obj['2']]);
+    });
+
+    it('should prevent infinite digest if track by expression is stable', function() {
+      scope.makeOptions = function() {
+          var options = [];
+          for (var i = 0; i < 5; i++) {
+              options.push({ label: 'Value = ' + i, value: i });
+          }
+          return options;
+      };
+      scope.selected = { label: 'Value = 1', value: 1 };
+      expect(function() {
+        createSelect({
+          'ng-model': 'selected',
+          'ng-options': 'item.label for item in makeOptions() track by item.value'
+        });
+      }).not.toThrow();
+    });
+  });
+
+
+  /**
+   * This behavior is broken and should probably be cleaned up later as track by and select as
+   * aren't compatible.
+   */
+  describe('selectAs+trackBy expression', function() {
+    beforeEach(function() {
+      scope.arr = [{subItem: {label: 'ten', id: 10}}, {subItem: {label: 'twenty', id: 20}}];
+      scope.obj = {'10': {subItem: {id: 10, label: 'ten'}}, '20': {subItem: {id: 20, label: 'twenty'}}};
+    });
+
+
+    it('It should use the "value" variable to represent items in the array as well as for the ' +
+        'selected values in track by expression (single&array)', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)'
+      });
+
+      // First test model -> view
+
+      scope.$apply(function() {
+        scope.selected = scope.arr[0].subItem;
+      });
+      expect(element.val()).toEqual('10');
+
+      scope.$apply(function() {
+        scope.selected = scope.arr[1].subItem;
+      });
+      expect(element.val()).toEqual('20');
+
+      // Now test view -> model
+
+      element.val('10');
+      browserTrigger(element, 'change');
+      expect(scope.selected).toBe(scope.arr[0].subItem);
+
+      // Now reload the array
+      scope.$apply(function() {
+        scope.arr = [{
+          subItem: {label: 'new ten', id: 10}
+        },{
+          subItem: {label: 'new twenty', id: 20}
+        }];
+      });
+      expect(element.val()).toBe('10');
+      expect(scope.selected.id).toBe(10);
+    });
+
+
+    it('It should use the "value" variable to represent items in the array as well as for the ' +
+        'selected values in track by expression (multiple&array)', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'multiple': true,
+        'ng-options': 'item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)'
+      });
+
+      // First test model -> view
+
+      scope.$apply(function() {
+        scope.selected = [scope.arr[0].subItem];
+      });
+      expect(element.val()).toEqual(['10']);
+
+      scope.$apply(function() {
+        scope.selected = [scope.arr[1].subItem];
+      });
+      expect(element.val()).toEqual(['20']);
+
+      // Now test view -> model
+
+      element.find('option')[0].selected = true;
+      element.find('option')[1].selected = false;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual([scope.arr[0].subItem]);
+
+      // Now reload the array
+      scope.$apply(function() {
+        scope.arr = [{
+          subItem: {label: 'new ten', id: 10}
+        },{
+          subItem: {label: 'new twenty', id: 20}
+        }];
+      });
+      expect(element.val()).toEqual(['10']);
+      expect(scope.selected[0].id).toEqual(10);
+      expect(scope.selected.length).toBe(1);
+    });
+
+
+    it('It should use the "value" variable to represent items in the array as well as for the ' +
+        'selected values in track by expression (multiple&object)', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'multiple': true,
+        'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)'
+      });
+
+      // First test model -> view
+
+      scope.$apply(function() {
+        scope.selected = [scope.obj['10'].subItem];
+      });
+      expect(element.val()).toEqual(['10']);
+
+
+      scope.$apply(function() {
+        scope.selected = [scope.obj['10'].subItem];
+      });
+      expect(element.val()).toEqual(['10']);
+
+      // Now test view -> model
+
+      element.find('option')[0].selected = true;
+      element.find('option')[1].selected = false;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual([scope.obj['10'].subItem]);
+
+      // Now reload the object
+      scope.$apply(function() {
+        scope.obj = {
+          '10': {
+            subItem: {label: 'new ten', id: 10}
+          },
+          '20': {
+            subItem: {label: 'new twenty', id: 20}
+          }
+        };
+      });
+      expect(element.val()).toEqual(['10']);
+      expect(scope.selected[0].id).toBe(10);
+      expect(scope.selected.length).toBe(1);
+    });
+
+
+    it('It should use the "value" variable to represent items in the array as well as for the ' +
+        'selected values in track by expression (single&object)', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)'
+      });
+
+      // First test model -> view
+
+      scope.$apply(function() {
+        scope.selected = scope.obj['10'].subItem;
+      });
+      expect(element.val()).toEqual('10');
+
+
+      scope.$apply(function() {
+        scope.selected = scope.obj['10'].subItem;
+      });
+      expect(element.val()).toEqual('10');
+
+      // Now test view -> model
+
+      element.find('option')[0].selected = true;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual(scope.obj['10'].subItem);
+
+      // Now reload the object
+      scope.$apply(function() {
+        scope.obj = {
+          '10': {
+            subItem: {label: 'new ten', id: 10}
+          },
+          '20': {
+            subItem: {label: 'new twenty', id: 20}
+          }
+        };
+      });
+      expect(element.val()).toEqual('10');
+      expect(scope.selected.id).toBe(10);
+    });
+  });
+
+
+  describe('binding', function() {
+
+    it('should bind to scope value', function() {
+      createSingleSelect();
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}, {name: 'B'}];
+        scope.selected = scope.values[0];
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+
+      scope.$apply(function() {
+        scope.selected = scope.values[1];
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+    });
+
+
+    it('should bind to scope value and group', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.name group by item.group for item in values'
+      });
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'},
+                        {name: 'B', group: 'first'},
+                        {name: 'C', group: 'second'},
+                        {name: 'D', group: 'first'},
+                        {name: 'E', group: 'second'}];
+        scope.selected = scope.values[3];
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+
+      var first = jqLite(element.find('optgroup')[0]);
+      var b = jqLite(first.find('option')[0]);
+      var d = jqLite(first.find('option')[1]);
+      expect(first.attr('label')).toEqual('first');
+      expect(b.text()).toEqual('B');
+      expect(d.text()).toEqual('D');
+
+      var second = jqLite(element.find('optgroup')[1]);
+      var c = jqLite(second.find('option')[0]);
+      var e = jqLite(second.find('option')[1]);
+      expect(second.attr('label')).toEqual('second');
+      expect(c.text()).toEqual('C');
+      expect(e.text()).toEqual('E');
+
+      scope.$apply(function() {
+        scope.selected = scope.values[0];
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+    });
+
+
+    it('should place non-grouped items in the list where they appear', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.name group by item.group for item in values'
+      });
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'},
+                        {name: 'B', group: 'first'},
+                        {name: 'C', group: 'second'},
+                        {name: 'D'},
+                        {name: 'E', group: 'first'},
+                        {name: 'F'},
+                        {name: 'G'},
+                        {name: 'H', group: 'second'}];
+        scope.selected = scope.values[0];
+      });
+
+      var children = element.children();
+      expect(children.length).toEqual(6);
+
+      expect(nodeName_(children[0])).toEqual('option');
+      expect(nodeName_(children[1])).toEqual('optgroup');
+      expect(nodeName_(children[2])).toEqual('optgroup');
+      expect(nodeName_(children[3])).toEqual('option');
+      expect(nodeName_(children[4])).toEqual('option');
+      expect(nodeName_(children[5])).toEqual('option');
+    });
+
+
+    it('should bind to scope value and track/identify objects', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.name for item in values track by item.id'
+      });
+
+      scope.$apply(function() {
+        scope.values = [{id: 1, name: 'first'},
+                        {id: 2, name: 'second'},
+                        {id: 3, name: 'third'},
+                        {id: 4, name: 'forth'}];
+        scope.selected = scope.values[1];
+      });
+
+      expect(element.val()).toEqual('2');
+
+      var first = jqLite(element.find('option')[0]);
+      expect(first.text()).toEqual('first');
+      expect(first.attr('value')).toEqual('1');
+      var forth = jqLite(element.find('option')[3]);
+      expect(forth.text()).toEqual('forth');
+      expect(forth.attr('value')).toEqual('4');
+
+      scope.$apply(function() {
+        scope.selected = scope.values[3];
+      });
+
+      expect(element.val()).toEqual('4');
+    });
+
+
+    it('should bind to scope value through expression', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.id as item.name for item in values'
+      });
+
+      scope.$apply(function() {
+        scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
+        scope.selected = scope.values[0].id;
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+
+      scope.$apply(function() {
+        scope.selected = scope.values[1].id;
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+    });
+
+
+    it('should update options in the DOM', function() {
+      compile(
+        '<select ng-model="selected" ng-options="item.id as item.name for item in values"></select>'
+      );
+
+      scope.$apply(function() {
+        scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
+        scope.selected = scope.values[0].id;
+      });
+
+      scope.$apply(function() {
+        scope.values[0].name = 'C';
+      });
+
+      var options = element.find('option');
+      expect(options.length).toEqual(2);
+      expect(options.eq(0)).toEqualOption(10, 'C');
+      expect(options.eq(1)).toEqualOption(20, 'B');
+    });
+
+
+    it('should update options in the DOM from object source', function() {
+      compile(
+        '<select ng-model="selected" ng-options="val.id as val.name for (key, val) in values"></select>'
+      );
+
+      scope.$apply(function() {
+        scope.values = {a: {id: 10, name: 'A'}, b: {id: 20, name: 'B'}};
+        scope.selected = scope.values.a.id;
+      });
+
+      scope.$apply(function() {
+        scope.values.a.name = 'C';
+      });
+
+      var options = element.find('option');
+      expect(options.length).toEqual(2);
+      expect(options.eq(0)).toEqualOption(10, 'C');
+      expect(options.eq(1)).toEqualOption(20, 'B');
+    });
+
+
+    it('should bind to object key', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'key as value for (key, value) in object'
+      });
+
+      scope.$apply(function() {
+        scope.object = {red: 'FF0000', green: '00FF00', blue: '0000FF'};
+        scope.selected = 'green';
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+
+      scope.$apply(function() {
+        scope.selected = 'blue';
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+    });
+
+
+    it('should bind to object value', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'value as key for (key, value) in object'
+      });
+
+      scope.$apply(function() {
+        scope.object = {red: 'FF0000', green: '00FF00', blue:'0000FF'};
+        scope.selected = '00FF00';
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+
+      scope.$apply(function() {
+        scope.selected = '0000FF';
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+    });
+
+
+    it('should insert a blank option if bound to null', function() {
+      createSingleSelect();
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}];
+        scope.selected = null;
+      });
+
+      expect(element.find('option').length).toEqual(2);
+      expect(element.val()).toEqual('');
+      expect(jqLite(element.find('option')[0]).val()).toEqual('');
+
+      scope.$apply(function() {
+        scope.selected = scope.values[0];
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+      expect(element.find('option').length).toEqual(1);
+    });
+
+
+    it('should reuse blank option if bound to null', function() {
+      createSingleSelect(true);
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}];
+        scope.selected = null;
+      });
+
+      expect(element.find('option').length).toEqual(2);
+      expect(element.val()).toEqual('');
+      expect(jqLite(element.find('option')[0]).val()).toEqual('');
+
+      scope.$apply(function() {
+        scope.selected = scope.values[0];
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+      expect(element.find('option').length).toEqual(2);
+    });
+
+
+    it('should not insert a blank option if one of the options maps to null', function() {
+      createSelect({
+        'ng-model': 'myColor',
+        'ng-options': 'color.shade as color.name for color in colors'
+      });
+
+      scope.$apply(function() {
+        scope.colors = [
+          {name:'nothing', shade:null},
+          {name:'red', shade:'dark'}
+        ];
+        scope.myColor = null;
+      });
+
+      expect(element.find('option').length).toEqual(2);
+      expect(element.find('option').eq(0)).toEqualOption(null);
+      expect(element.val()).not.toEqualUnknownValue(null);
+      expect(element.find('option').eq(0)).not.toEqualUnknownOption(null);
+    });
+
+
+    it('should insert a unknown option if bound to something not in the list', function() {
+      createSingleSelect();
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}];
+        scope.selected = {};
+      });
+
+      expect(element.find('option').length).toEqual(2);
+      expect(element.val()).toEqualUnknownValue(scope.selected);
+      expect(element.find('option').eq(0)).toEqualUnknownOption(scope.selected);
+
+      scope.$apply(function() {
+        scope.selected = scope.values[0];
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+      expect(element.find('option').length).toEqual(1);
+    });
+
+
+    it('should select correct input if previously selected option was "?"', function() {
+      createSingleSelect();
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}, {name: 'B'}];
+        scope.selected = {};
+      });
+
+      expect(element.find('option').length).toEqual(3);
+      expect(element.val()).toEqualUnknownValue();
+      expect(element.find('option').eq(0)).toEqualUnknownOption();
+
+      browserTrigger(element.find('option').eq(1));
+      expect(element.find('option').length).toEqual(2);
+      expect(element).toEqualSelectValue(scope.selected);
+      expect(element.find('option').eq(0).prop('selected')).toBeTruthy();
+    });
+
+
+    it('should use exact same values as values in scope with one-time bindings', function() {
+      scope.values = [{name: 'A'}, {name: 'B'}];
+      scope.selected = scope.values[0];
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'value.name for value in ::values'
+      });
+
+      browserTrigger(element.find('option').eq(1));
+
+      expect(scope.selected).toBe(scope.values[1]);
+    });
+
+
+    it('should ensure that at least one option element has the "selected" attribute', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.id as item.name for item in values'
+      });
+
+      scope.$apply(function() {
+        scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
+      });
+      expect(element.val()).toEqualUnknownValue();
+      expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
+
+      scope.$apply(function() {
+        scope.selected = 10;
+      });
+      // Here the ? option should disappear and the first real option should have selected attribute
+      expect(element).toEqualSelectValue(scope.selected);
+      expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
+
+      // Here the selected value is changed and we change the selected attribute
+      scope.$apply(function() {
+        scope.selected = 20;
+      });
+      expect(element).toEqualSelectValue(scope.selected);
+      expect(element.find('option').eq(1).attr('selected')).toEqual('selected');
+
+      scope.$apply(function() {
+        scope.values.push({id: 30, name: 'C'});
+      });
+      expect(element).toEqualSelectValue(scope.selected);
+      expect(element.find('option').eq(1).attr('selected')).toEqual('selected');
+
+      // Here the ? option should reappear and have selected attribute
+      scope.$apply(function() {
+        scope.selected = undefined;
+      });
+      expect(element.val()).toEqualUnknownValue();
+      expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
+    });
+
+
+    it('should select the correct option for selectAs and falsy values', function() {
+      scope.values = [{value: 0, label: 'zero'}, {value: 1, label: 'one'}];
+      scope.selected = '';
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'option.value as option.label for option in values'
+      });
+
+      var option = element.find('option').eq(0);
+      expect(option).toEqualUnknownOption();
+    });
+
+
+    it('should update the model if the selected option is removed', function() {
+      scope.values = [{value: 0, label: 'zero'}, {value: 1, label: 'one'}];
+      scope.selected = 1;
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'option.value as option.label for option in values'
+      });
+      expect(element).toEqualSelectValue(1);
+
+      // Check after initial option update
+      scope.$apply(function() {
+        scope.values.pop();
+      });
+
+      expect(element.val()).toEqualUnknownValue();
+      expect(scope.selected).toEqual(null);
+
+      // Check after model change
+      scope.$apply(function() {
+        scope.selected = 0;
+      });
+
+      expect(element).toEqualSelectValue(0);
+
+      scope.$apply(function() {
+        scope.values.pop();
+      });
+
+      expect(element.val()).toEqualUnknownValue();
+      expect(scope.selected).toEqual(null);
+    });
+
+
+    it('should update the model if all the selected (multiple) options are removed', function() {
+      scope.values = [{value: 0, label: 'zero'}, {value: 1, label: 'one'}, {value: 2, label: 'two'}];
+      scope.selected = [1, 2];
+      createSelect({
+        'ng-model': 'selected',
+        'multiple': true,
+        'ng-options': 'option.value as option.label for option in values'
+      });
+
+      expect(element).toEqualSelectValue([1, 2], true);
+
+      // Check after initial option update
+      scope.$apply(function() {
+        scope.values.pop();
+      });
+
+      expect(element).toEqualSelectValue([1], true);
+      expect(scope.selected).toEqual([1]);
+
+      scope.$apply(function() {
+        scope.values.pop();
+      });
+
+      expect(element).toEqualSelectValue([], true);
+      expect(scope.selected).toEqual([]);
+
+      // Check after model change
+      scope.$apply(function() {
+        scope.selected = [0];
+      });
+
+      expect(element).toEqualSelectValue([0], true);
+
+      scope.$apply(function() {
+        scope.values.pop();
+      });
+
+      expect(element).toEqualSelectValue([], true);
+      expect(scope.selected).toEqual([]);
+    });
+
+  });
+
+
+  describe('blank option', function() {
+
+    it('should be compiled as template, be watched and updated', function() {
+      var option;
+      createSingleSelect('<option value="">blank is {{blankVal}}</option>');
+
+      scope.$apply(function() {
+        scope.blankVal = 'so blank';
+        scope.values = [{name: 'A'}];
+      });
+
+      // check blank option is first and is compiled
+      expect(element.find('option').length).toBe(2);
+      option = element.find('option').eq(0);
+      expect(option.val()).toBe('');
+      expect(option.text()).toBe('blank is so blank');
+
+      scope.$apply(function() {
+        scope.blankVal = 'not so blank';
+      });
+
+      // check blank option is first and is compiled
+      expect(element.find('option').length).toBe(2);
+      option = element.find('option').eq(0);
+      expect(option.val()).toBe('');
+      expect(option.text()).toBe('blank is not so blank');
+    });
+
+
+    it('should support binding via ngBindTemplate directive', function() {
+      var option;
+      createSingleSelect('<option value="" ng-bind-template="blank is {{blankVal}}"></option>');
+
+      scope.$apply(function() {
+        scope.blankVal = 'so blank';
+        scope.values = [{name: 'A'}];
+      });
+
+      // check blank option is first and is compiled
+      expect(element.find('option').length).toBe(2);
+      option = element.find('option').eq(0);
+      expect(option.val()).toBe('');
+      expect(option.text()).toBe('blank is so blank');
+    });
+
+
+    it('should support biding via ngBind attribute', function() {
+      var option;
+      createSingleSelect('<option value="" ng-bind="blankVal"></option>');
+
+      scope.$apply(function() {
+        scope.blankVal = 'is blank';
+        scope.values = [{name: 'A'}];
+      });
+
+      // check blank option is first and is compiled
+      expect(element.find('option').length).toBe(2);
+      option = element.find('option').eq(0);
+      expect(option.val()).toBe('');
+      expect(option.text()).toBe('is blank');
+    });
+
+
+    it('should be rendered with the attributes preserved', function() {
+      var option;
+      createSingleSelect('<option value="" class="coyote" id="road-runner" ' +
+        'custom-attr="custom-attr">{{blankVal}}</option>');
+
+      scope.$apply(function() {
+        scope.blankVal = 'is blank';
+      });
+
+      // check blank option is first and is compiled
+      option = element.find('option').eq(0);
+      expect(option.hasClass('coyote')).toBeTruthy();
+      expect(option.attr('id')).toBe('road-runner');
+      expect(option.attr('custom-attr')).toBe('custom-attr');
+    });
+
+    it('should be selected, if it is available and no other option is selected', function() {
+      // selectedIndex is used here because jqLite incorrectly reports element.val()
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}];
+      });
+      createSingleSelect(true);
+      // ensure the first option (the blank option) is selected
+      expect(element[0].selectedIndex).toEqual(0);
+      scope.$digest();
+      // ensure the option has not changed following the digest
+      expect(element[0].selectedIndex).toEqual(0);
+    });
+  });
+
+
+  describe('on change', function() {
+
+    it('should update model on change', function() {
+      createSingleSelect();
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}, {name: 'B'}];
+        scope.selected = scope.values[0];
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+
+      setSelectValue(element, 1);
+      expect(scope.selected).toEqual(scope.values[1]);
+    });
+
+
+    it('should update model on change through expression', function() {
+      createSelect({
+        'ng-model': 'selected',
+        'ng-options': 'item.id as item.name for item in values'
+      });
+
+      scope.$apply(function() {
+        scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
+        scope.selected = scope.values[0].id;
+      });
+
+      expect(element).toEqualSelectValue(scope.selected);
+
+      setSelectValue(element, 1);
+      expect(scope.selected).toEqual(scope.values[1].id);
+    });
+
+
+    it('should update model to null on change', function() {
+      createSingleSelect(true);
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}, {name: 'B'}];
+        scope.selected = scope.values[0];
+      });
+
+      element.val('');
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual(null);
+    });
+
+
+    // Regression https://github.com/angular/angular.js/issues/7855
+    it('should update the model with ng-change', function() {
+      createSelect({
+        'ng-change':'change()',
+        'ng-model':'selected',
+        'ng-options':'value for value in values'
+      });
+
+      scope.$apply(function() {
+        scope.values = ['A', 'B'];
+        scope.selected = 'A';
+      });
+
+      scope.change = function() {
+        scope.selected = 'A';
+      };
+
+      element.find('option')[1].selected = true;
+
+      browserTrigger(element, 'change');
+      expect(element.find('option')[0].selected).toBeTruthy();
+      expect(scope.selected).toEqual('A');
+    });
+  });
+
+  describe('disabled blank', function() {
+    it('should select disabled blank by default', function() {
+      var html = '<select ng-model="someModel" ng-options="c for c in choices">' +
+                   '<option value="" disabled>Choose One</option>' +
+                 '</select>';
+      scope.$apply(function() {
+        scope.choices = ['A', 'B', 'C'];
+      });
+
+      compile(html);
+
+      var options = element.find('option');
+      var optionToSelect = options.eq(0);
+      expect(optionToSelect.text()).toBe('Choose One');
+      expect(optionToSelect.prop('selected')).toBe(true);
+      expect(element[0].value).toBe('');
+
+      dealoc(element);
+    });
+
+
+    it('should select disabled blank by default when select is required', function() {
+      var html = '<select ng-model="someModel" ng-options="c for c in choices" required>' +
+                   '<option value="" disabled>Choose One</option>' +
+                 '</select>';
+      scope.$apply(function() {
+        scope.choices = ['A', 'B', 'C'];
+      });
+
+      compile(html);
+
+      var options = element.find('option');
+      var optionToSelect = options.eq(0);
+      expect(optionToSelect.text()).toBe('Choose One');
+      expect(optionToSelect.prop('selected')).toBe(true);
+      expect(element[0].value).toBe('');
+
+      dealoc(element);
+    });
+  });
+
+  describe('select-many', function() {
+
+    it('should read multiple selection', function() {
+      createMultiSelect();
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}, {name: 'B'}];
+        scope.selected = [];
+      });
+
+      expect(element.find('option').length).toEqual(2);
+      expect(element.find('option')[0].selected).toBeFalsy();
+      expect(element.find('option')[1].selected).toBeFalsy();
+
+      scope.$apply(function() {
+        scope.selected.push(scope.values[1]);
+      });
+
+      expect(element.find('option').length).toEqual(2);
+      expect(element.find('option')[0].selected).toBeFalsy();
+      expect(element.find('option')[1].selected).toBeTruthy();
+
+      scope.$apply(function() {
+        scope.selected.push(scope.values[0]);
+      });
+
+      expect(element.find('option').length).toEqual(2);
+      expect(element.find('option')[0].selected).toBeTruthy();
+      expect(element.find('option')[1].selected).toBeTruthy();
+    });
+
+
+    it('should update model on change', function() {
+      createMultiSelect();
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}, {name: 'B'}];
+        scope.selected = [];
+      });
+
+      element.find('option')[0].selected = true;
+
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual([scope.values[0]]);
+    });
+
+
+    it('should select from object', function() {
+      createSelect({
+        'ng-model':'selected',
+        'multiple':true,
+        'ng-options':'key as value for (key,value) in values'
+      });
+      scope.values = {'0':'A', '1':'B'};
+
+      scope.selected = ['1'];
+      scope.$digest();
+      expect(element.find('option')[1].selected).toBe(true);
+
+      element.find('option')[0].selected = true;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual(['0', '1']);
+
+      element.find('option')[1].selected = false;
+      browserTrigger(element, 'change');
+      expect(scope.selected).toEqual(['0']);
+    });
+
+    it('should deselect all options when model is emptied', function() {
+      createMultiSelect();
+      scope.$apply(function() {
+        scope.values = [{name: 'A'}, {name: 'B'}];
+        scope.selected = [scope.values[0]];
+      });
+      expect(element.find('option')[0].selected).toEqual(true);
+
+      scope.$apply(function() {
+        scope.selected.pop();
+      });
+
+      expect(element.find('option')[0].selected).toEqual(false);
+    });
+  });
+
+
+  describe('ngRequired', function() {
+
+    it('should allow bindings on ngRequired', function() {
+      createSelect({
+        'ng-model': 'value',
+        'ng-options': 'item.name for item in values',
+        'ng-required': 'required'
+      }, true);
+
+
+      scope.$apply(function() {
+        scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}];
+        scope.required = false;
+      });
+
+      element.val('');
+      browserTrigger(element, 'change');
+      expect(element).toBeValid();
+
+      scope.$apply(function() {
+        scope.required = true;
+      });
+      expect(element).toBeInvalid();
+
+      scope.$apply(function() {
+        scope.value = scope.values[0];
+      });
+      expect(element).toBeValid();
+
+      element.val('');
+      browserTrigger(element, 'change');
+      expect(element).toBeInvalid();
+
+      scope.$apply(function() {
+        scope.required = false;
+      });
+      expect(element).toBeValid();
+    });
+
+
+    it('should treat an empty array as invalid when `multiple` attribute used', function() {
+      createSelect({
+        'ng-model': 'value',
+        'ng-options': 'item.name for item in values',
+        'ng-required': 'required',
+        'multiple': ''
+      }, true);
+
+      scope.$apply(function() {
+        scope.value = [];
+        scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}];
+        scope.required = true;
+      });
+      expect(element).toBeInvalid();
+
+      scope.$apply(function() {
+        // ngModelWatch does not set objectEquality flag
+        // array must be replaced in order to trigger $formatters
+        scope.value = [scope.values[0]];
+      });
+      expect(element).toBeValid();
+    });
+
+
+    it('should allow falsy values as values', function() {
+      createSelect({
+        'ng-model': 'value',
+        'ng-options': 'item.value as item.name for item in values',
+        'ng-required': 'required'
+      }, true);
+
+      scope.$apply(function() {
+        scope.values = [{name: 'True', value: true}, {name: 'False', value: false}];
+        scope.required = false;
+      });
+
+      setSelectValue(element, 2);
+      expect(element).toBeValid();
+      expect(scope.value).toBe(false);
+
+      scope.$apply('required = true');
+      expect(element).toBeValid();
+      expect(scope.value).toBe(false);
+    });
+  });
+
+  describe('ngModelCtrl', function() {
+    it('should prefix the model value with the word "the" using $parsers', function() {
+      createSelect({
+        'name': 'select',
+        'ng-model': 'value',
+        'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
+      });
+
+      scope.form.select.$parsers.push(function(value) {
+        return 'the ' + value;
+      });
+
+      setSelectValue(element, 3);
+      expect(scope.value).toBe('the third');
+      expect(element).toEqualSelectValue('third');
+    });
+
+    it('should prefix the view value with the word "the" using $formatters', function() {
+      createSelect({
+        'name': 'select',
+        'ng-model': 'value',
+        'ng-options': 'item for item in [\'the first\', \'the second\', \'the third\', \'the fourth\']'
+      });
+
+      scope.form.select.$formatters.push(function(value) {
+        return 'the ' + value;
+      });
+
+      scope.$apply(function() {
+        scope.value = 'third';
+      });
+      expect(element).toEqualSelectValue('the third');
+    });
+
+    it('should fail validation when $validators fail', function() {
+      createSelect({
+        'name': 'select',
+        'ng-model': 'value',
+        'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
+      });
+
+      scope.form.select.$validators.fail = function() {
+        return false;
+      };
+
+      setSelectValue(element, 3);
+      expect(element).toBeInvalid();
+      expect(scope.value).toBeUndefined();
+      expect(element).toEqualSelectValue('third');
+    });
+
+    it('should pass validation when $validators pass', function() {
+      createSelect({
+        'name': 'select',
+        'ng-model': 'value',
+        'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
+      });
+
+      scope.form.select.$validators.pass = function() {
+        return true;
+      };
+
+      setSelectValue(element, 3);
+      expect(element).toBeValid();
+      expect(scope.value).toBe('third');
+      expect(element).toEqualSelectValue('third');
+    });
+
+    it('should fail validation when $asyncValidators fail', inject(function($q, $rootScope) {
+      var defer;
+      createSelect({
+        'name': 'select',
+        'ng-model': 'value',
+        'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
+      });
+
+      scope.form.select.$asyncValidators.async = function() {
+        defer = $q.defer();
+        return defer.promise;
+      };
+
+      setSelectValue(element, 3);
+      expect(scope.form.select.$pending).toBeDefined();
+      expect(scope.value).toBeUndefined();
+      expect(element).toEqualSelectValue('third');
+
+      defer.reject();
+      $rootScope.$digest();
+      expect(scope.form.select.$pending).toBeUndefined();
+      expect(scope.value).toBeUndefined();
+      expect(element).toEqualSelectValue('third');
+    }));
+
+    it('should pass validation when $asyncValidators pass', inject(function($q, $rootScope) {
+      var defer;
+      createSelect({
+        'name': 'select',
+        'ng-model': 'value',
+        'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
+      });
+
+      scope.form.select.$asyncValidators.async = function() {
+        defer = $q.defer();
+        return defer.promise;
+      };
+
+      setSelectValue(element, 3);
+      expect(scope.form.select.$pending).toBeDefined();
+      expect(scope.value).toBeUndefined();
+      expect(element).toEqualSelectValue('third');
+
+      defer.resolve();
+      $rootScope.$digest();
+      expect(scope.form.select.$pending).toBeUndefined();
+      expect(scope.value).toBe('third');
+      expect(element).toEqualSelectValue('third');
+    }));
+  });
+});
diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js
index 2a624130fc69..725c87ddec97 100644
--- a/test/ng/directive/selectSpec.js
+++ b/test/ng/directive/selectSpec.js
@@ -10,6 +10,10 @@ describe('select', function() {
     scope.$apply();
   }
 
+  function unknownValue(value) {
+    return '? ' + hashKey(value) + ' ?';
+  }
+
   beforeEach(inject(function($rootScope, _$compile_) {
     scope = $rootScope.$new(); //create a child scope because the root scope can't be $destroy-ed
     $compile = _$compile_;
@@ -47,7 +51,8 @@ describe('select', function() {
         forEach(this.actual.find('option'), function(option) {
           optionGroup = option.parentNode.label || '';
           actualValues[optionGroup] = actualValues[optionGroup] || [];
-          actualValues[optionGroup].push(option.label);
+          // IE9 doesn't populate the label property from the text property like other browsers
+          actualValues[optionGroup].push(option.label || option.text);
         });
 
         this.message = function() {
@@ -55,25 +60,6 @@ describe('select', function() {
         };
 
         return equals(expected, actualValues);
-      },
-
-      toEqualOption: function(value, text, label) {
-        var errors = [];
-        if (this.actual.attr('value') !== value) {
-          errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "' + value + '"');
-        }
-        if (text && this.actual.text() !== text) {
-          errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "' + value + '"');
-        }
-        if (label && this.actual.attr('label') !== label) {
-          errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "' + value + '"');
-        }
-
-        this.message = function() {
-          return errors.join('\n');
-        };
-
-        return errors.length === 0;
       }
     });
 
@@ -250,31 +236,6 @@ describe('select', function() {
         expect(scope.robot).toBe('');
       });
 
-      it('should not be set when an option is selected and options are set asynchronously',
-        inject(function($timeout) {
-          compile('<select ng-model="model" ng-options="opt.id as opt.label for opt in options">' +
-                      '</select>');
-
-          scope.$apply(function() {
-            scope.model = 0;
-          });
-
-          $timeout(function() {
-            scope.options = [
-              {id: 0, label: 'x'},
-              {id: 1, label: 'y'}
-            ];
-          }, 0);
-
-          $timeout.flush();
-
-          var options = element.find('option');
-
-          expect(options.length).toEqual(2);
-          expect(options.eq(0)).toEqualOption('0', 'x');
-          expect(options.eq(1)).toEqualOption('1', 'y');
-        })
-      );
 
       describe('interactions with repeated options', function() {
 
@@ -333,7 +294,7 @@ describe('select', function() {
                   '<option>r2d2</option>' +
                 '</select>');
 
-        expect(element).toEqualSelect(['? undefined:undefined ?'], 'c3p0', 'r2d2');
+        expect(element).toEqualSelect([unknownValue(undefined)], 'c3p0', 'r2d2');
 
         scope.$apply(function() {
           scope.robot = 'r2d2';
@@ -344,7 +305,7 @@ describe('select', function() {
         scope.$apply(function() {
           scope.robot = "wallee";
         });
-        expect(element).toEqualSelect(['? string:wallee ?'], 'c3p0', 'r2d2');
+        expect(element).toEqualSelect([unknownValue('wallee')], 'c3p0', 'r2d2');
       });
 
 
@@ -362,7 +323,7 @@ describe('select', function() {
         scope.$apply(function() {
           scope.robot = null;
         });
-        expect(element).toEqualSelect(['? object:null ?'], '', 'c3p0', 'r2d2');
+        expect(element).toEqualSelect([unknownValue(null)], '', 'c3p0', 'r2d2');
 
         scope.$apply(function() {
           scope.robot = 'r2d2';
@@ -385,7 +346,7 @@ describe('select', function() {
                   '<option>r2d2</option>' +
                 '</select>');
 
-        expect(element).toEqualSelect(['? string:wallee ?'], '', 'c3p0', 'r2d2');
+        expect(element).toEqualSelect([unknownValue('wallee')], '', 'c3p0', 'r2d2');
 
         scope.$apply(function() {
           scope.robot = 'r2d2';
@@ -400,13 +361,13 @@ describe('select', function() {
           compile('<select ng-model="robot">' +
                     '<option ng-repeat="r in robots">{{r}}</option>' +
                   '</select>');
-          expect(element).toEqualSelect(['? undefined:undefined ?']);
+          expect(element).toEqualSelect([unknownValue(undefined)]);
           expect(scope.robot).toBeUndefined();
 
           scope.$apply(function() {
             scope.robot = 'r2d2';
           });
-          expect(element).toEqualSelect(['? string:r2d2 ?']);
+          expect(element).toEqualSelect([unknownValue('r2d2')]);
           expect(scope.robot).toBe('r2d2');
 
           scope.$apply(function() {
@@ -428,7 +389,7 @@ describe('select', function() {
           scope.$apply(function() {
             scope.robot = 'r2d2';
           });
-          expect(element).toEqualSelect(['? string:r2d2 ?'], '');
+          expect(element).toEqualSelect([unknownValue('r2d2')], '');
           expect(scope.robot).toBe('r2d2');
 
           scope.$apply(function() {
@@ -452,7 +413,7 @@ describe('select', function() {
           scope.$apply(function() {
             scope.robots.pop();
           });
-          expect(element).toEqualSelect(['? string:r2d2 ?'], 'c3p0');
+          expect(element).toEqualSelect([unknownValue('r2d2')], 'c3p0');
           expect(scope.robot).toBe('r2d2');
 
           scope.$apply(function() {
@@ -464,1941 +425,405 @@ describe('select', function() {
           scope.$apply(function() {
             delete scope.robots;
           });
-          expect(element).toEqualSelect(['? string:r2d2 ?']);
+          expect(element).toEqualSelect([unknownValue('r2d2')]);
           expect(scope.robot).toBe('r2d2');
         });
-
-        describe('selectController.hasOption', function() {
-          it('should return false for options shifted via ngOptions', function() {
-            scope.robots = [
-              {value: 1, label: 'c3p0'},
-              {value: 2, label: 'r2d2'}
-            ];
-
-            compile('<select ng-model="robot" ' +
-                      'ng-options="item.value as item.label for item in robots">' +
-                    '</select>');
-
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.robots.shift();
-            });
-
-            expect(selectCtrl.hasOption('c3p0')).toBe(false);
-            expect(selectCtrl.hasOption('r2d2')).toBe(true);
-          });
-
-
-          it('should return false for options popped via ngOptions', function() {
-            scope.robots = [
-              {value: 1, label: 'c3p0'},
-              {value: 2, label: 'r2d2'}
-            ];
-
-            compile('<select ng-model="robot" ' +
-                      'ng-options="item.value as item.label for item in robots">' +
-                    '</select>');
-
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.robots.pop();
-            });
-
-            expect(selectCtrl.hasOption('c3p0')).toBe(true);
-            expect(selectCtrl.hasOption('r2d2')).toBe(false);
-          });
-
-
-          it('should return true for options added via ngOptions', function() {
-            scope.robots = [
-              {value: 2, label: 'r2d2'}
-            ];
-
-            compile('<select ng-model="robot" ' +
-                      'ng-options="item.value as item.label for item in robots">' +
-                    '</select>');
-
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.robots.unshift({value: 1, label: 'c3p0'});
-            });
-
-            expect(selectCtrl.hasOption('c3p0')).toBe(true);
-            expect(selectCtrl.hasOption('r2d2')).toBe(true);
-          });
-
-
-          it('should keep all the options when changing the model', function() {
-            compile('<select ng-model="mySelect" ng-options="o for o in [\'A\',\'B\',\'C\']"></select>');
-            var selectCtrl = element.controller('select');
-            scope.$apply(function() {
-              scope.mySelect = 'C';
-            });
-            expect(selectCtrl.hasOption('A')).toBe(true);
-            expect(selectCtrl.hasOption('B')).toBe(true);
-            expect(selectCtrl.hasOption('C')).toBe(true);
-            expect(element).toEqualSelectWithOptions({'': ['A', 'B', 'C']});
-          });
-
-
-          it('should be able to detect when elements move from a previous group', function() {
-            scope.values = [
-              {name: 'A'},
-              {name: 'B', group: 'first'},
-              {name: 'C', group: 'first'},
-              {name: 'D', group: 'first'},
-              {name: 'E', group: 'second'}
-            ];
-
-            compile('<select ng-model="mySelect" ng-options="item.name group by item.group for item in values"></select>');
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.values[3] = {name: 'D', group: 'second'};
-              scope.values.shift();
-            });
-            expect(selectCtrl.hasOption('A')).toBe(false);
-            expect(selectCtrl.hasOption('B')).toBe(true);
-            expect(selectCtrl.hasOption('C')).toBe(true);
-            expect(selectCtrl.hasOption('D')).toBe(true);
-            expect(selectCtrl.hasOption('E')).toBe(true);
-            expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C'], 'second': ['D', 'E']});
-          });
-
-
-          it('should be able to detect when elements move from a following group', function() {
-            scope.values = [
-              {name: 'A'},
-              {name: 'B', group: 'first'},
-              {name: 'C', group: 'first'},
-              {name: 'D', group: 'second'},
-              {name: 'E', group: 'second'}
-            ];
-
-            compile('<select ng-model="mySelect" ng-options="item.name group by item.group for item in values"></select>');
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.values[3].group = 'first';
-              scope.values.shift();
-            });
-            expect(selectCtrl.hasOption('A')).toBe(false);
-            expect(selectCtrl.hasOption('B')).toBe(true);
-            expect(selectCtrl.hasOption('C')).toBe(true);
-            expect(selectCtrl.hasOption('D')).toBe(true);
-            expect(selectCtrl.hasOption('E')).toBe(true);
-            expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C', 'D'], 'second': ['E']});
-          });
-
-
-          it('should be able to detect when an element is replaced with an element from a previous group', function() {
-            scope.values = [
-              {name: 'A'},
-              {name: 'B', group: 'first'},
-              {name: 'C', group: 'first'},
-              {name: 'D', group: 'first'},
-              {name: 'E', group: 'second'},
-              {name: 'F', group: 'second'}
-            ];
-
-            compile('<select ng-model="mySelect" ng-options="item.name group by item.group for item in values"></select>');
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.values[3].group = 'second';
-              scope.values.pop();
-            });
-            expect(selectCtrl.hasOption('A')).toBe(true);
-            expect(selectCtrl.hasOption('B')).toBe(true);
-            expect(selectCtrl.hasOption('C')).toBe(true);
-            expect(selectCtrl.hasOption('D')).toBe(true);
-            expect(selectCtrl.hasOption('E')).toBe(true);
-            expect(selectCtrl.hasOption('F')).toBe(false);
-            expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['D', 'E']});
-          });
-
-
-          it('should be able to detect when element is replaced with an element from a following group', function() {
-            scope.values = [
-              {name: 'A'},
-              {name: 'B', group: 'first'},
-              {name: 'C', group: 'first'},
-              {name: 'D', group: 'second'},
-              {name: 'E', group: 'second'}
-            ];
-
-            compile('<select ng-model="mySelect" ng-options="item.name group by item.group for item in values"></select>');
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.values[3].group = 'first';
-              scope.values.splice(2, 1);
-            });
-            expect(selectCtrl.hasOption('A')).toBe(true);
-            expect(selectCtrl.hasOption('B')).toBe(true);
-            expect(selectCtrl.hasOption('C')).toBe(false);
-            expect(selectCtrl.hasOption('D')).toBe(true);
-            expect(selectCtrl.hasOption('E')).toBe(true);
-            expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'D'], 'second': ['E']});
-          });
-
-
-          it('should be able to detect when an element is removed', function() {
-            scope.values = [
-              {name: 'A'},
-              {name: 'B', group: 'first'},
-              {name: 'C', group: 'first'},
-              {name: 'D', group: 'second'},
-              {name: 'E', group: 'second'}
-            ];
-
-            compile('<select ng-model="mySelect" ng-options="item.name group by item.group for item in values"></select>');
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.values.splice(3, 1);
-            });
-            expect(selectCtrl.hasOption('A')).toBe(true);
-            expect(selectCtrl.hasOption('B')).toBe(true);
-            expect(selectCtrl.hasOption('C')).toBe(true);
-            expect(selectCtrl.hasOption('D')).toBe(false);
-            expect(selectCtrl.hasOption('E')).toBe(true);
-            expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['E']});
-          });
-
-
-          it('should be able to detect when a group is removed', function() {
-            scope.values = [
-              {name: 'A'},
-              {name: 'B', group: 'first'},
-              {name: 'C', group: 'first'},
-              {name: 'D', group: 'second'},
-              {name: 'E', group: 'second'}
-            ];
-
-            compile('<select ng-model="mySelect" ng-options="item.name group by item.group for item in values"></select>');
-            var selectCtrl = element.data().$selectController;
-
-            scope.$apply(function() {
-              scope.values.splice(3, 2);
-            });
-            expect(selectCtrl.hasOption('A')).toBe(true);
-            expect(selectCtrl.hasOption('B')).toBe(true);
-            expect(selectCtrl.hasOption('C')).toBe(true);
-            expect(selectCtrl.hasOption('D')).toBe(false);
-            expect(selectCtrl.hasOption('E')).toBe(false);
-            expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C']});
-          });
-        });
-      });
-    });
-  });
-
-
-  describe('select-multiple', function() {
-
-    it('should support type="select-multiple"', function() {
-      compile(
-        '<select ng-model="selection" multiple>' +
-          '<option>A</option>' +
-          '<option>B</option>' +
-        '</select>');
-
-      scope.$apply(function() {
-        scope.selection = ['A'];
-      });
-
-      expect(element).toEqualSelect(['A'], 'B');
-
-      scope.$apply(function() {
-        scope.selection.push('B');
       });
 
-      expect(element).toEqualSelect(['A'], ['B']);
     });
 
-    it('should work with optgroups', function() {
-      compile('<select ng-model="selection" multiple>' +
-                '<optgroup label="group1">' +
-                  '<option>A</option>' +
-                  '<option>B</option>' +
-                '</optgroup>' +
-              '</select>');
+  });
 
-      expect(element).toEqualSelect('A', 'B');
-      expect(scope.selection).toBeUndefined();
 
-      scope.$apply(function() {
-        scope.selection = ['A'];
-      });
-      expect(element).toEqualSelect(['A'], 'B');
+  describe('selectController.hasOption', function() {
 
-      scope.$apply(function() {
-        scope.selection.push('B');
-      });
-      expect(element).toEqualSelect(['A'], ['B']);
-    });
+    function compileRepeatedOptions() {
+      compile('<select ng-model="robot">' +
+                '<option value="{{item.value}}" ng-repeat="item in robots">{{item.label}}</option>' +
+              '</select>');
+    }
 
-    it('should require', function() {
+    function compileGroupedOptions() {
       compile(
-        '<select name="select" ng-model="selection" multiple required>' +
-          '<option>A</option>' +
-          '<option>B</option>' +
+        '<select ng-model="mySelect">' +
+          '<option ng-repeat="item in values">{{item.name}}</option>' +
+          '<optgroup ng-repeat="group in groups" label="{{group.name}}">' +
+            '<option ng-repeat="item in group.values">{{item.name}}</option>' +
+          '</optgroup>' +
         '</select>');
+    }
 
-      scope.$apply(function() {
-        scope.selection = [];
-      });
-
-      expect(scope.form.select.$error.required).toBeTruthy();
-      expect(element).toBeInvalid();
-      expect(element).toBePristine();
-
-      scope.$apply(function() {
-        scope.selection = ['A'];
-      });
-
-      expect(element).toBeValid();
-      expect(element).toBePristine();
-
-      element[0].value = 'B';
-      browserTrigger(element, 'change');
-      expect(element).toBeValid();
-      expect(element).toBeDirty();
-    });
-
-    describe('selectController.hasOption', function() {
-      it('should return false for options shifted via ngOptions', function() {
+    describe('flat options', function() {
+      it('should return false for options shifted via ngRepeat', function() {
         scope.robots = [
           {value: 1, label: 'c3p0'},
           {value: 2, label: 'r2d2'}
         ];
 
-        compile('<select ng-model="robot" multiple ' +
-                  'ng-options="item.value as item.label for item in robots">' +
-                '</select>');
+        compileRepeatedOptions();
 
-        var selectCtrl = element.data().$selectController;
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
           scope.robots.shift();
         });
 
-        expect(selectCtrl.hasOption('c3p0')).toBe(false);
-        expect(selectCtrl.hasOption('r2d2')).toBe(true);
+        expect(selectCtrl.hasOption('1')).toBe(false);
+        expect(selectCtrl.hasOption('2')).toBe(true);
       });
 
-      it('should return false for options popped via ngOptions', function() {
+
+      it('should return false for options popped via ngRepeat', function() {
         scope.robots = [
           {value: 1, label: 'c3p0'},
           {value: 2, label: 'r2d2'}
         ];
 
-        compile('<select ng-model="robot" multiple ' +
-                  'ng-options="item.value as item.label for item in robots">' +
-                '</select>');
+        compileRepeatedOptions();
 
-        var selectCtrl = element.data().$selectController;
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
           scope.robots.pop();
         });
 
-        expect(selectCtrl.hasOption('c3p0')).toBe(true);
-        expect(selectCtrl.hasOption('r2d2')).toBe(false);
+        expect(selectCtrl.hasOption('1')).toBe(true);
+        expect(selectCtrl.hasOption('2')).toBe(false);
       });
 
-      it('should return true for options added via ngOptions', function() {
+
+      it('should return true for options added via ngRepeat', function() {
         scope.robots = [
           {value: 2, label: 'r2d2'}
         ];
 
-        compile('<select ng-model="robot" multiple ' +
-                  'ng-options="item.value as item.label for item in robots">' +
-                '</select>');
+        compileRepeatedOptions();
 
-        var selectCtrl = element.data().$selectController;
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
           scope.robots.unshift({value: 1, label: 'c3p0'});
         });
 
-        expect(selectCtrl.hasOption('c3p0')).toBe(true);
-        expect(selectCtrl.hasOption('r2d2')).toBe(true);
-      });
-    });
-  });
-
-
-  describe('ngOptions', function() {
-    function createSelect(attrs, blank, unknown) {
-      var html = '<select';
-      forEach(attrs, function(value, key) {
-        if (isBoolean(value)) {
-          if (value) html += ' ' + key;
-        } else {
-          html += ' ' + key + '="' + value + '"';
-        }
+        expect(selectCtrl.hasOption('1')).toBe(true);
+        expect(selectCtrl.hasOption('2')).toBe(true);
       });
-      html += '>' +
-        (blank ? (isString(blank) ? blank : '<option value="">blank</option>') : '') +
-        (unknown ? (isString(unknown) ? unknown : '<option value="?">unknown</option>') : '') +
-      '</select>';
-
-      compile(html);
-    }
 
-    function createSingleSelect(blank, unknown) {
-      createSelect({
-        'ng-model':'selected',
-        'ng-options':'value.name for value in values'
-      }, blank, unknown);
-    }
 
-    function createMultiSelect(blank, unknown) {
-      createSelect({
-        'ng-model':'selected',
-        'multiple':true,
-        'ng-options':'value.name for value in values'
-      }, blank, unknown);
-    }
+      it('should keep all the options when changing the model', function() {
 
-    describe('selectAs expression', function() {
-      beforeEach(function() {
-        scope.arr = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}];
-        scope.obj = {'10': {score: 10, label: 'ten'}, '20': {score: 20, label: 'twenty'}};
-      });
+        compile('<select ng-model="mySelect"><option ng-repeat="o in [\'A\',\'B\',\'C\']">{{o}}</option></select>');
 
-      it('should support single select with array source', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.id as item.label for item in arr'
-        });
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
-          scope.selected = 10;
+          scope.mySelect = 'C';
         });
-        expect(element.val()).toBe('0');
 
-        element.val('1');
-        browserTrigger(element, 'change');
-        expect(scope.selected).toBe(20);
+        expect(selectCtrl.hasOption('A')).toBe(true);
+        expect(selectCtrl.hasOption('B')).toBe(true);
+        expect(selectCtrl.hasOption('C')).toBe(true);
+        expect(element).toEqualSelectWithOptions({'': ['A', 'B', 'C']});
       });
+    });
 
 
-      it('should support multi select with array source', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'multiple': true,
-          'ng-options': 'item.id as item.label for item in arr'
-        });
-
-        scope.$apply(function() {
-          scope.selected = [10,20];
-        });
-        expect(element.val()).toEqual(['0','1']);
-        expect(scope.selected).toEqual([10,20]);
+    describe('grouped options', function() {
 
-        element.children()[0].selected = false;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual([20]);
-        expect(element.val()).toEqual(['1']);
-      });
+      it('should be able to detect when elements move from a previous group', function() {
+        scope.values = [{name: 'A'}];
+        scope.groups = [
+          {
+            name: 'first',
+            values: [
+              {name: 'B'},
+              {name: 'C'},
+              {name: 'D'}
+            ]
+          },
+          {
+            name: 'second',
+            values: [
+              {name: 'E'}
+            ]
+          }
+        ];
 
+        compileGroupedOptions();
 
-      it('should support single select with object source', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'val.score as val.label for (key, val) in obj'
-        });
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
-          scope.selected = 10;
+          var itemD = scope.groups[0].values.pop();
+          scope.groups[1].values.unshift(itemD);
+          scope.values.shift();
         });
-        expect(element.val()).toBe('10');
 
-        element.val('20');
-        browserTrigger(element, 'change');
-        expect(scope.selected).toBe(20);
+        expect(selectCtrl.hasOption('A')).toBe(false);
+        expect(selectCtrl.hasOption('B')).toBe(true);
+        expect(selectCtrl.hasOption('C')).toBe(true);
+        expect(selectCtrl.hasOption('D')).toBe(true);
+        expect(selectCtrl.hasOption('E')).toBe(true);
+        expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C'], 'second': ['D', 'E']});
       });
 
 
-      it('should support multi select with object source', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'multiple': true,
-          'ng-options': 'val.score as val.label for (key, val) in obj'
-        });
+      it('should be able to detect when elements move from a following group', function() {
+        scope.values = [{name: 'A'}];
+        scope.groups = [
+          {
+            name: 'first',
+            values: [
+              {name: 'B'},
+              {name: 'C'}
+            ]
+          },
+          {
+            name: 'second',
+            values: [
+              {name: 'D'},
+              {name: 'E'}
+            ]
+          }
+        ];
+
+        compileGroupedOptions();
+
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
-          scope.selected = [10,20];
+          var itemD = scope.groups[1].values.shift();
+          scope.groups[0].values.push(itemD);
+          scope.values.shift();
         });
-        expect(element.val()).toEqual(['10','20']);
-
-        element.children()[0].selected = false;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual([20]);
-        expect(element.val()).toEqual(['20']);
+        expect(selectCtrl.hasOption('A')).toBe(false);
+        expect(selectCtrl.hasOption('B')).toBe(true);
+        expect(selectCtrl.hasOption('C')).toBe(true);
+        expect(selectCtrl.hasOption('D')).toBe(true);
+        expect(selectCtrl.hasOption('E')).toBe(true);
+        expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C', 'D'], 'second': ['E']});
       });
-    });
 
 
-    describe('trackBy expression', function() {
-      beforeEach(function() {
-        scope.arr = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}];
-        scope.obj = {'1': {score: 10, label: 'ten'}, '2': {score: 20, label: 'twenty'}};
-      });
-
+      it('should be able to detect when an element is replaced with an element from a previous group', function() {
+        scope.values = [{name: 'A'}];
+        scope.groups = [
+          {
+            name: 'first',
+            values: [
+              {name: 'B'},
+              {name: 'C'},
+              {name: 'D'}
+            ]
+          },
+          {
+            name: 'second',
+            values: [
+              {name: 'E'},
+              {name: 'F'}
+            ]
+          }
+        ];
 
-      it('should set the result of track by expression to element value', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.label for item in arr track by item.id'
-        });
+        compileGroupedOptions();
 
-        scope.$apply(function() {
-          scope.selected = scope.arr[0];
-        });
-        expect(element.val()).toBe('10');
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
-          scope.arr[0] = {id: 10, label: 'new ten'};
+          var itemD = scope.groups[0].values.pop();
+          scope.groups[1].values.unshift(itemD);
+          scope.groups[1].values.pop();
         });
-        expect(element.val()).toBe('10');
-
-        element.children()[1].selected = 'selected';
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual(scope.arr[1]);
+        expect(selectCtrl.hasOption('A')).toBe(true);
+        expect(selectCtrl.hasOption('B')).toBe(true);
+        expect(selectCtrl.hasOption('C')).toBe(true);
+        expect(selectCtrl.hasOption('D')).toBe(true);
+        expect(selectCtrl.hasOption('E')).toBe(true);
+        expect(selectCtrl.hasOption('F')).toBe(false);
+        expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['D', 'E']});
       });
 
 
-      it('should use the tracked expression as option value', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.label for item in arr track by item.id'
-        });
-
-        var options = element.find('option');
-        expect(options.length).toEqual(3);
-        expect(options.eq(0)).toEqualOption('?', '');
-        expect(options.eq(1)).toEqualOption('10', 'ten');
-        expect(options.eq(2)).toEqualOption('20', 'twenty');
-      });
+      it('should be able to detect when element is replaced with an element from a following group', function() {
+        scope.values = [{name: 'A'}];
+        scope.groups = [
+          {
+            name: 'first',
+            values: [
+              {name: 'B'},
+              {name: 'C'}
+            ]
+          },
+          {
+            name: 'second',
+            values: [
+              {name: 'D'},
+              {name: 'E'}
+            ]
+          }
+        ];
 
-      it('should preserve value even when reference has changed (single&array)', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.label for item in arr track by item.id'
-        });
+        compileGroupedOptions();
 
-        scope.$apply(function() {
-          scope.selected = scope.arr[0];
-        });
-        expect(element.val()).toBe('10');
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
-          scope.arr[0] = {id: 10, label: 'new ten'};
+          scope.groups[0].values.pop();
+          var itemD = scope.groups[1].values.shift();
+          scope.groups[0].values.push(itemD);
         });
-        expect(element.val()).toBe('10');
-
-        element.children()[1].selected = 1;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual(scope.arr[1]);
+        expect(selectCtrl.hasOption('A')).toBe(true);
+        expect(selectCtrl.hasOption('B')).toBe(true);
+        expect(selectCtrl.hasOption('C')).toBe(false);
+        expect(selectCtrl.hasOption('D')).toBe(true);
+        expect(selectCtrl.hasOption('E')).toBe(true);
+        expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'D'], 'second': ['E']});
       });
 
 
-      it('should preserve value even when reference has changed (multi&array)', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'multiple': true,
-          'ng-options': 'item.label for item in arr track by item.id'
-        });
+      it('should be able to detect when an element is removed', function() {
+        scope.values = [{name: 'A'}];
+        scope.groups = [
+          {
+            name: 'first',
+            values: [
+              {name: 'B'},
+              {name: 'C'}
+            ]
+          },
+          {
+            name: 'second',
+            values: [
+              {name: 'D'},
+              {name: 'E'}
+            ]
+          }
+        ];
 
-        scope.$apply(function() {
-          scope.selected = scope.arr;
-        });
-        expect(element.val()).toEqual(['10','20']);
+        compileGroupedOptions();
+
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
-          scope.arr[0] = {id: 10, label: 'new ten'};
+          scope.groups[1].values.shift();
         });
-        expect(element.val()).toEqual(['10','20']);
-
-        element.children()[0].selected = false;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual([scope.arr[1]]);
+        expect(selectCtrl.hasOption('A')).toBe(true);
+        expect(selectCtrl.hasOption('B')).toBe(true);
+        expect(selectCtrl.hasOption('C')).toBe(true);
+        expect(selectCtrl.hasOption('D')).toBe(false);
+        expect(selectCtrl.hasOption('E')).toBe(true);
+        expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['E']});
       });
 
 
-      it('should preserve value even when reference has changed (single&object)', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'val.label for (key, val) in obj track by val.score'
-        });
+      it('should be able to detect when a group is removed', function() {
+        scope.values = [{name: 'A'}];
+        scope.groups = [
+          {
+            name: 'first',
+            values: [
+              {name: 'B'},
+              {name: 'C'}
+            ]
+          },
+          {
+            name: 'second',
+            values: [
+              {name: 'D'},
+              {name: 'E'}
+            ]
+          }
+        ];
 
-        scope.$apply(function() {
-          scope.selected = scope.obj['1'];
-        });
-        expect(element.val()).toBe('10');
+        compileGroupedOptions();
+
+        var selectCtrl = element.controller('select');
 
         scope.$apply(function() {
-          scope.obj['1'] = {score: 10, label: 'ten'};
+          scope.groups.pop();
         });
-        expect(element.val()).toBe('10');
-
-        element.val('20');
-        browserTrigger(element, 'change');
-        expect(scope.selected).toBe(scope.obj['2']);
+        expect(selectCtrl.hasOption('A')).toBe(true);
+        expect(selectCtrl.hasOption('B')).toBe(true);
+        expect(selectCtrl.hasOption('C')).toBe(true);
+        expect(selectCtrl.hasOption('D')).toBe(false);
+        expect(selectCtrl.hasOption('E')).toBe(false);
+        expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C']});
       });
+    });
+  });
 
+  describe('select-multiple', function() {
 
-      it('should preserve value even when reference has changed (multi&object)', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'multiple': true,
-          'ng-options': 'val.label for (key, val) in obj track by val.score'
-        });
-
-        scope.$apply(function() {
-          scope.selected = [scope.obj['1']];
-        });
-        expect(element.val()).toEqual(['10']);
-
-        scope.$apply(function() {
-          scope.obj['1'] = {score: 10, label: 'ten'};
-        });
-        expect(element.val()).toEqual(['10']);
+    it('should support type="select-multiple"', function() {
+      compile(
+        '<select ng-model="selection" multiple>' +
+          '<option>A</option>' +
+          '<option>B</option>' +
+        '</select>');
 
-        element.children()[1].selected = 'selected';
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual([scope.obj['1'], scope.obj['2']]);
+      scope.$apply(function() {
+        scope.selection = ['A'];
       });
-    });
 
+      expect(element).toEqualSelect(['A'], 'B');
 
-    /**
-     * This behavior is broken and should probably be cleaned up later as track by and select as
-     * aren't compatible.
-     */
-    describe('selectAs+trackBy expression', function() {
-      beforeEach(function() {
-        scope.arr = [{subItem: {label: 'ten', id: 10}}, {subItem: {label: 'twenty', id: 20}}];
-        scope.obj = {'10': {subItem: {id: 10, label: 'ten'}}, '20': {subItem: {id: 20, label: 'twenty'}}};
+      scope.$apply(function() {
+        scope.selection.push('B');
       });
 
+      expect(element).toEqualSelect(['A'], ['B']);
+    });
 
-      it('It should use the "value" variable to represent items in the array as well as for the ' +
-          'selected values in track by expression (single&array)', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)'
-        });
-
-        // First test model -> view
+    it('should work with optgroups', function() {
+      compile('<select ng-model="selection" multiple>' +
+                '<optgroup label="group1">' +
+                  '<option>A</option>' +
+                  '<option>B</option>' +
+                '</optgroup>' +
+              '</select>');
 
-        scope.$apply(function() {
-          scope.selected = scope.arr[0].subItem;
-        });
-        expect(element.val()).toEqual('10');
+      expect(element).toEqualSelect('A', 'B');
+      expect(scope.selection).toBeUndefined();
 
-        scope.$apply(function() {
-          scope.selected = scope.arr[1].subItem;
-        });
-        expect(element.val()).toEqual('20');
+      scope.$apply(function() {
+        scope.selection = ['A'];
+      });
+      expect(element).toEqualSelect(['A'], 'B');
 
-        // Now test view -> model
+      scope.$apply(function() {
+        scope.selection.push('B');
+      });
+      expect(element).toEqualSelect(['A'], ['B']);
+    });
 
-        element.val('10');
-        browserTrigger(element, 'change');
-        expect(scope.selected).toBe(scope.arr[0].subItem);
+    it('should require', function() {
+      compile(
+        '<select name="select" ng-model="selection" multiple required>' +
+          '<option>A</option>' +
+          '<option>B</option>' +
+        '</select>');
 
-        // Now reload the array
-        scope.$apply(function() {
-          scope.arr = [{
-            subItem: {label: 'new ten', id: 10}
-          },{
-            subItem: {label: 'new twenty', id: 20}
-          }];
-        });
-        expect(element.val()).toBe('10');
-        expect(scope.selected.id).toBe(10);
+      scope.$apply(function() {
+        scope.selection = [];
       });
 
+      expect(scope.form.select.$error.required).toBeTruthy();
+      expect(element).toBeInvalid();
+      expect(element).toBePristine();
 
-      it('It should use the "value" variable to represent items in the array as well as for the ' +
-          'selected values in track by expression (multiple&array)', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'multiple': true,
-          'ng-options': 'item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)'
-        });
+      scope.$apply(function() {
+        scope.selection = ['A'];
+      });
 
-        // First test model -> view
-
-        scope.$apply(function() {
-          scope.selected = [scope.arr[0].subItem];
-        });
-        expect(element.val()).toEqual(['10']);
-
-        scope.$apply(function() {
-          scope.selected = [scope.arr[1].subItem];
-        });
-        expect(element.val()).toEqual(['20']);
-
-        // Now test view -> model
-
-        element.find('option')[0].selected = true;
-        element.find('option')[1].selected = false;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual([scope.arr[0].subItem]);
-
-        // Now reload the array
-        scope.$apply(function() {
-          scope.arr = [{
-            subItem: {label: 'new ten', id: 10}
-          },{
-            subItem: {label: 'new twenty', id: 20}
-          }];
-        });
-        expect(element.val()).toEqual(['10']);
-        expect(scope.selected[0].id).toEqual(10);
-        expect(scope.selected.length).toBe(1);
-      });
-
-
-      it('It should use the "value" variable to represent items in the array as well as for the ' +
-          'selected values in track by expression (multiple&object)', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'multiple': true,
-          'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)'
-        });
-
-        // First test model -> view
-
-        scope.$apply(function() {
-          scope.selected = [scope.obj['10'].subItem];
-        });
-        expect(element.val()).toEqual(['10']);
-
-
-        scope.$apply(function() {
-          scope.selected = [scope.obj['10'].subItem];
-        });
-        expect(element.val()).toEqual(['10']);
-
-        // Now test view -> model
-
-        element.find('option')[0].selected = true;
-        element.find('option')[1].selected = false;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual([scope.obj['10'].subItem]);
-
-        // Now reload the object
-        scope.$apply(function() {
-          scope.obj = {
-            '10': {
-              subItem: {label: 'new ten', id: 10}
-            },
-            '20': {
-              subItem: {label: 'new twenty', id: 20}
-            }
-          };
-        });
-        expect(element.val()).toEqual(['10']);
-        expect(scope.selected[0].id).toBe(10);
-        expect(scope.selected.length).toBe(1);
-      });
-
-
-      it('It should use the "value" variable to represent items in the array as well as for the ' +
-          'selected values in track by expression (single&object)', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)'
-        });
-
-        // First test model -> view
-
-        scope.$apply(function() {
-          scope.selected = scope.obj['10'].subItem;
-        });
-        expect(element.val()).toEqual('10');
-
-
-        scope.$apply(function() {
-          scope.selected = scope.obj['10'].subItem;
-        });
-        expect(element.val()).toEqual('10');
-
-        // Now test view -> model
-
-        element.find('option')[0].selected = true;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual(scope.obj['10'].subItem);
-
-        // Now reload the object
-        scope.$apply(function() {
-          scope.obj = {
-            '10': {
-              subItem: {label: 'new ten', id: 10}
-            },
-            '20': {
-              subItem: {label: 'new twenty', id: 20}
-            }
-          };
-        });
-        expect(element.val()).toEqual('10');
-        expect(scope.selected.id).toBe(10);
-      });
-    });
-
-
-    it('should throw when not formated "? for ? in ?"', function() {
-      expect(function() {
-          compile('<select ng-model="selected" ng-options="i dont parse"></select>');
-        }).toThrowMinErr('ngOptions', 'iexp', /Expected expression in form of/);
-    });
-
-
-    it('should render a list', function() {
-      createSingleSelect();
-
-      scope.$apply(function() {
-        scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
-        scope.selected = scope.values[0];
-      });
-
-      var options = element.find('option');
-      expect(options.length).toEqual(3);
-      expect(options.eq(0)).toEqualOption('0', 'A');
-      expect(options.eq(1)).toEqualOption('1', 'B');
-      expect(options.eq(2)).toEqualOption('2', 'C');
-    });
-
-    it('should render zero as a valid display value', function() {
-      createSingleSelect();
-
-      scope.$apply(function() {
-        scope.values = [{name: 0}, {name: 1}, {name: 2}];
-        scope.selected = scope.values[0];
-      });
-
-      var options = element.find('option');
-      expect(options.length).toEqual(3);
-      expect(options.eq(0)).toEqualOption('0', '0');
-      expect(options.eq(1)).toEqualOption('1', '1');
-      expect(options.eq(2)).toEqualOption('2', '2');
-    });
-
-
-    it('should render an object', function() {
-      createSelect({
-        'ng-model': 'selected',
-        'ng-options': 'value as key for (key, value) in object'
-      });
-
-      scope.$apply(function() {
-        scope.object = {'red': 'FF0000', 'green': '00FF00', 'blue': '0000FF'};
-        scope.selected = scope.object.red;
-      });
-
-      var options = element.find('option');
-      expect(options.length).toEqual(3);
-      expect(options.eq(0)).toEqualOption('blue', 'blue');
-      expect(options.eq(1)).toEqualOption('green', 'green');
-      expect(options.eq(2)).toEqualOption('red', 'red');
-      expect(options[2].selected).toEqual(true);
-
-      scope.$apply(function() {
-        scope.object.azur = '8888FF';
-      });
-
-      options = element.find('option');
-      expect(options[3].selected).toEqual(true);
-    });
-
-
-    it('should grow list', function() {
-      createSingleSelect();
-
-      scope.$apply(function() {
-        scope.values = [];
-      });
-
-      expect(element.find('option').length).toEqual(1); // because we add special empty option
-      expect(element.find('option')).toEqualOption('?','');
-
-      scope.$apply(function() {
-        scope.values.push({name:'A'});
-        scope.selected = scope.values[0];
-      });
-
-      expect(element.find('option').length).toEqual(1);
-      expect(element.find('option')).toEqualOption('0', 'A');
-
-      scope.$apply(function() {
-        scope.values.push({name:'B'});
-      });
-
-      expect(element.find('option').length).toEqual(2);
-      expect(element.find('option').eq(0)).toEqualOption('0', 'A');
-      expect(element.find('option').eq(1)).toEqualOption('1', 'B');
-    });
-
-
-    it('should shrink list', function() {
-      createSingleSelect();
-
-      scope.$apply(function() {
-        scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
-        scope.selected = scope.values[0];
-      });
-
-      expect(element.find('option').length).toEqual(3);
-
-      scope.$apply(function() {
-        scope.values.pop();
-      });
-
-      expect(element.find('option').length).toEqual(2);
-      expect(element.find('option').eq(0)).toEqualOption('0', 'A');
-      expect(element.find('option').eq(1)).toEqualOption('1', 'B');
-
-      scope.$apply(function() {
-        scope.values.pop();
-      });
-
-      expect(element.find('option').length).toEqual(1);
-      expect(element.find('option')).toEqualOption('0', 'A');
-
-      scope.$apply(function() {
-        scope.values.pop();
-        scope.selected = null;
-      });
-
-      expect(element.find('option').length).toEqual(1); // we add back the special empty option
-    });
-
-
-    it('should shrink and then grow list', function() {
-      createSingleSelect();
-
-      scope.$apply(function() {
-        scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
-        scope.selected = scope.values[0];
-      });
-
-      expect(element.find('option').length).toEqual(3);
-
-      scope.$apply(function() {
-        scope.values = [{name: '1'}, {name: '2'}];
-        scope.selected = scope.values[0];
-      });
-
-      expect(element.find('option').length).toEqual(2);
-
-      scope.$apply(function() {
-        scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
-        scope.selected = scope.values[0];
-      });
-
-      expect(element.find('option').length).toEqual(3);
-    });
-
-
-    it('should update list', function() {
-      createSingleSelect();
-
-      scope.$apply(function() {
-        scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
-        scope.selected = scope.values[0];
-      });
-
-      scope.$apply(function() {
-        scope.values = [{name: 'B'}, {name: 'C'}, {name: 'D'}];
-        scope.selected = scope.values[0];
-      });
-
-      var options = element.find('option');
-      expect(options.length).toEqual(3);
-      expect(options.eq(0)).toEqualOption('0', 'B');
-      expect(options.eq(1)).toEqualOption('1', 'C');
-      expect(options.eq(2)).toEqualOption('2', 'D');
-    });
-
-
-    it('should preserve existing options', function() {
-      createSingleSelect(true);
-
-      scope.$apply(function() {
-        scope.values = [];
-      });
-
-      expect(element.find('option').length).toEqual(1);
-
-      scope.$apply(function() {
-        scope.values = [{name: 'A'}];
-        scope.selected = scope.values[0];
-      });
-
-      expect(element.find('option').length).toEqual(2);
-      expect(jqLite(element.find('option')[0]).text()).toEqual('blank');
-      expect(jqLite(element.find('option')[1]).text()).toEqual('A');
-
-      scope.$apply(function() {
-        scope.values = [];
-        scope.selected = null;
-      });
-
-      expect(element.find('option').length).toEqual(1);
-      expect(jqLite(element.find('option')[0]).text()).toEqual('blank');
-    });
-
-    it('should ignore $ and $$ properties', function() {
-      createSelect({
-        'ng-options': 'key as value for (key, value) in object',
-        'ng-model': 'selected'
-      });
-
-      scope.$apply(function() {
-        scope.object = {'regularProperty': 'visible', '$$private': 'invisible', '$property': 'invisible'};
-        scope.selected = 'regularProperty';
-      });
-
-      var options = element.find('option');
-      expect(options.length).toEqual(1);
-      expect(options.eq(0)).toEqualOption('regularProperty', 'visible');
-    });
-
-    it('should allow expressions over multiple lines', function() {
-      scope.isNotFoo = function(item) {
-        return item.name !== 'Foo';
-      };
-
-      createSelect({
-        'ng-options': 'key.id\n' +
-          'for key in object\n' +
-          '| filter:isNotFoo',
-        'ng-model': 'selected'
-      });
-
-      scope.$apply(function() {
-        scope.object = [{'id': 1, 'name': 'Foo'},
-                        {'id': 2, 'name': 'Bar'},
-                        {'id': 3, 'name': 'Baz'}];
-        scope.selected = scope.object[0];
-      });
-
-      var options = element.find('option');
-      expect(options.length).toEqual(3);
-      expect(options.eq(1)).toEqualOption('0', '2');
-      expect(options.eq(2)).toEqualOption('1', '3');
-    });
-
-    it('should not update selected property of an option element on digest with no change event',
-        function() {
-      // ng-options="value.name for value in values"
-      // ng-model="selected"
-      createSingleSelect();
-
-      scope.$apply(function() {
-        scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
-        scope.selected = scope.values[0];
-      });
-
-      var options = element.find('option');
-
-      expect(scope.selected).toEqual({ name: 'A' });
-      expect(options.eq(0).prop('selected')).toBe(true);
-      expect(options.eq(1).prop('selected')).toBe(false);
-
-      var optionToSelect = options.eq(1);
-
-      expect(optionToSelect.text()).toBe('B');
-
-      optionToSelect.prop('selected', true);
-      scope.$digest();
-
-      expect(optionToSelect.prop('selected')).toBe(true);
-      expect(scope.selected).toBe(scope.values[0]);
-    });
-
-    // bug fix #9621
-    it('should update the label property', function() {
-      // ng-options="value.name for value in values"
-      // ng-model="selected"
-      createSingleSelect();
-
-      scope.$apply(function() {
-        scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
-        scope.selected = scope.values[0];
-      });
-
-      var options = element.find('option');
-      expect(options.eq(0).prop('label')).toEqual('A');
-      expect(options.eq(1).prop('label')).toEqual('B');
-      expect(options.eq(2).prop('label')).toEqual('C');
-    });
-
-    describe('binding', function() {
-
-      it('should bind to scope value', function() {
-        createSingleSelect();
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}, {name: 'B'}];
-          scope.selected = scope.values[0];
-        });
-
-        expect(element.val()).toEqual('0');
-
-        scope.$apply(function() {
-          scope.selected = scope.values[1];
-        });
-
-        expect(element.val()).toEqual('1');
-      });
-
-
-      it('should bind to scope value and group', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.name group by item.group for item in values'
-        });
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'},
-                          {name: 'B', group: 'first'},
-                          {name: 'C', group: 'second'},
-                          {name: 'D', group: 'first'},
-                          {name: 'E', group: 'second'}];
-          scope.selected = scope.values[3];
-        });
-
-        expect(element.val()).toEqual('3');
-
-        var first = jqLite(element.find('optgroup')[0]);
-        var b = jqLite(first.find('option')[0]);
-        var d = jqLite(first.find('option')[1]);
-        expect(first.attr('label')).toEqual('first');
-        expect(b.text()).toEqual('B');
-        expect(d.text()).toEqual('D');
-
-        var second = jqLite(element.find('optgroup')[1]);
-        var c = jqLite(second.find('option')[0]);
-        var e = jqLite(second.find('option')[1]);
-        expect(second.attr('label')).toEqual('second');
-        expect(c.text()).toEqual('C');
-        expect(e.text()).toEqual('E');
-
-        scope.$apply(function() {
-          scope.selected = scope.values[0];
-        });
-
-        expect(element.val()).toEqual('0');
-      });
-
-
-      it('should bind to scope value and track/identify objects', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.name for item in values track by item.id'
-        });
-
-        scope.$apply(function() {
-          scope.values = [{id: 1, name: 'first'},
-                          {id: 2, name: 'second'},
-                          {id: 3, name: 'third'},
-                          {id: 4, name: 'forth'}];
-          scope.selected = scope.values[1];
-        });
-
-        expect(element.val()).toEqual('2');
-
-        var first = jqLite(element.find('option')[0]);
-        expect(first.text()).toEqual('first');
-        expect(first.attr('value')).toEqual('1');
-        var forth = jqLite(element.find('option')[3]);
-        expect(forth.text()).toEqual('forth');
-        expect(forth.attr('value')).toEqual('4');
-
-        scope.$apply(function() {
-          scope.selected = scope.values[3];
-        });
-
-        expect(element.val()).toEqual('4');
-      });
-
-
-      it('should bind to scope value through experession', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.id as item.name for item in values'
-        });
-
-        scope.$apply(function() {
-          scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
-          scope.selected = scope.values[0].id;
-        });
-
-        expect(element.val()).toEqual('0');
-
-        scope.$apply(function() {
-          scope.selected = scope.values[1].id;
-        });
-
-        expect(element.val()).toEqual('1');
-      });
-
-      it('should update options in the DOM', function() {
-        compile(
-          '<select ng-model="selected" ng-options="item.id as item.name for item in values"></select>'
-        );
-
-        scope.$apply(function() {
-          scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
-          scope.selected = scope.values[0].id;
-        });
-
-        scope.$apply(function() {
-          scope.values[0].name = 'C';
-        });
-
-        var options = element.find('option');
-        expect(options.length).toEqual(2);
-        expect(options.eq(0)).toEqualOption('0', 'C');
-        expect(options.eq(1)).toEqualOption('1', 'B');
-      });
-
-
-      it('should update options in the DOM from object source', function() {
-        compile(
-          '<select ng-model="selected" ng-options="val.id as val.name for (key, val) in values"></select>'
-        );
-
-        scope.$apply(function() {
-          scope.values = {a: {id: 10, name: 'A'}, b: {id: 20, name: 'B'}};
-          scope.selected = scope.values.a.id;
-        });
-
-        scope.$apply(function() {
-          scope.values.a.name = 'C';
-        });
-
-        var options = element.find('option');
-        expect(options.length).toEqual(2);
-        expect(options.eq(0)).toEqualOption('a', 'C');
-        expect(options.eq(1)).toEqualOption('b', 'B');
-      });
-
-
-      it('should bind to object key', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'key as value for (key, value) in object'
-        });
-
-        scope.$apply(function() {
-          scope.object = {red: 'FF0000', green: '00FF00', blue: '0000FF'};
-          scope.selected = 'green';
-        });
-
-        expect(element.val()).toEqual('green');
-
-        scope.$apply(function() {
-          scope.selected = 'blue';
-        });
-
-        expect(element.val()).toEqual('blue');
-      });
-
-
-      it('should bind to object value', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'value as key for (key, value) in object'
-        });
-
-        scope.$apply(function() {
-          scope.object = {red: 'FF0000', green: '00FF00', blue:'0000FF'};
-          scope.selected = '00FF00';
-        });
-
-        expect(element.val()).toEqual('green');
-
-        scope.$apply(function() {
-          scope.selected = '0000FF';
-        });
-
-        expect(element.val()).toEqual('blue');
-      });
-
-
-      it('should insert a blank option if bound to null', function() {
-        createSingleSelect();
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}];
-          scope.selected = null;
-        });
-
-        expect(element.find('option').length).toEqual(2);
-        expect(element.val()).toEqual('');
-        expect(jqLite(element.find('option')[0]).val()).toEqual('');
-
-        scope.$apply(function() {
-          scope.selected = scope.values[0];
-        });
-
-        expect(element.val()).toEqual('0');
-        expect(element.find('option').length).toEqual(1);
-      });
-
-
-      it('should reuse blank option if bound to null', function() {
-        createSingleSelect(true);
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}];
-          scope.selected = null;
-        });
-
-        expect(element.find('option').length).toEqual(2);
-        expect(element.val()).toEqual('');
-        expect(jqLite(element.find('option')[0]).val()).toEqual('');
-
-        scope.$apply(function() {
-          scope.selected = scope.values[0];
-        });
-
-        expect(element.val()).toEqual('0');
-        expect(element.find('option').length).toEqual(2);
-      });
-
-
-      it('should insert a unknown option if bound to something not in the list', function() {
-        createSingleSelect();
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}];
-          scope.selected = {};
-        });
-
-        expect(element.find('option').length).toEqual(2);
-        expect(element.val()).toEqual('?');
-        expect(jqLite(element.find('option')[0]).val()).toEqual('?');
-
-        scope.$apply(function() {
-          scope.selected = scope.values[0];
-        });
-
-        expect(element.val()).toEqual('0');
-        expect(element.find('option').length).toEqual(1);
-      });
-
-
-      it('should select correct input if previously selected option was "?"', function() {
-        createSingleSelect();
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}, {name: 'B'}];
-          scope.selected = {};
-        });
-
-        expect(element.find('option').length).toEqual(3);
-        expect(element.val()).toEqual('?');
-        expect(element.find('option').eq(0).val()).toEqual('?');
-
-        browserTrigger(element.find('option').eq(1));
-        expect(element.val()).toEqual('0');
-        expect(element.find('option').eq(0).prop('selected')).toBeTruthy();
-        expect(element.find('option').length).toEqual(2);
-      });
-
-
-      it('should use exact same values as values in scope with one-time bindings', function() {
-        scope.values = [{name: 'A'}, {name: 'B'}];
-        scope.selected = scope.values[0];
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'value.name for value in ::values'
-        });
-
-        browserTrigger(element.find('option').eq(1));
-
-        expect(scope.selected).toBe(scope.values[1]);
-      });
-
-
-      it('should ensure that at least one option element has the "selected" attribute', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.id as item.name for item in values'
-        });
-
-        scope.$apply(function() {
-          scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
-        });
-        expect(element.val()).toEqual('?');
-        expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
-
-        scope.$apply(function() {
-          scope.selected = 10;
-        });
-        // Here the ? option should disappear and the first real option should have selected attribute
-        expect(element.val()).toEqual('0');
-        expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
-
-        // Here the selected value is changed but we don't change the selected attribute
-        scope.$apply(function() {
-          scope.selected = 20;
-        });
-        expect(element.val()).toEqual('1');
-        expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
-
-        scope.$apply(function() {
-          scope.values.push({id: 30, name: 'C'});
-        });
-        expect(element.val()).toEqual('1');
-        expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
-
-        // Here the ? option should reappear and have selected attribute
-        scope.$apply(function() {
-          scope.selected = undefined;
-        });
-        expect(element.val()).toEqual('?');
-        expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
-      });
-
-
-      it('should select the correct option for selectAs and falsy values', function() {
-        scope.values = [{value: 0, label: 'zero'}, {value: 1, label: 'one'}];
-        scope.selected = '';
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'option.value as option.label for option in values'
-        });
-
-        var option = element.find('option').eq(0);
-        expect(option.val()).toBe('?');
-        expect(option.text()).toBe('');
-      });
-    });
-
-
-    describe('blank option', function() {
-
-      it('should be compiled as template, be watched and updated', function() {
-        var option;
-        createSingleSelect('<option value="">blank is {{blankVal}}</option>');
-
-        scope.$apply(function() {
-          scope.blankVal = 'so blank';
-          scope.values = [{name: 'A'}];
-        });
-
-        // check blank option is first and is compiled
-        expect(element.find('option').length).toBe(2);
-        option = element.find('option').eq(0);
-        expect(option.val()).toBe('');
-        expect(option.text()).toBe('blank is so blank');
-
-        scope.$apply(function() {
-          scope.blankVal = 'not so blank';
-        });
-
-        // check blank option is first and is compiled
-        expect(element.find('option').length).toBe(2);
-        option = element.find('option').eq(0);
-        expect(option.val()).toBe('');
-        expect(option.text()).toBe('blank is not so blank');
-      });
-
-
-      it('should support binding via ngBindTemplate directive', function() {
-        var option;
-        createSingleSelect('<option value="" ng-bind-template="blank is {{blankVal}}"></option>');
-
-        scope.$apply(function() {
-          scope.blankVal = 'so blank';
-          scope.values = [{name: 'A'}];
-        });
-
-        // check blank option is first and is compiled
-        expect(element.find('option').length).toBe(2);
-        option = element.find('option').eq(0);
-        expect(option.val()).toBe('');
-        expect(option.text()).toBe('blank is so blank');
-      });
-
-
-      it('should support biding via ngBind attribute', function() {
-        var option;
-        createSingleSelect('<option value="" ng-bind="blankVal"></option>');
-
-        scope.$apply(function() {
-          scope.blankVal = 'is blank';
-          scope.values = [{name: 'A'}];
-        });
-
-        // check blank option is first and is compiled
-        expect(element.find('option').length).toBe(2);
-        option = element.find('option').eq(0);
-        expect(option.val()).toBe('');
-        expect(option.text()).toBe('is blank');
-      });
-
-
-      it('should be rendered with the attributes preserved', function() {
-        var option;
-        createSingleSelect('<option value="" class="coyote" id="road-runner" ' +
-          'custom-attr="custom-attr">{{blankVal}}</option>');
-
-        scope.$apply(function() {
-          scope.blankVal = 'is blank';
-        });
-
-        // check blank option is first and is compiled
-        option = element.find('option').eq(0);
-        expect(option.hasClass('coyote')).toBeTruthy();
-        expect(option.attr('id')).toBe('road-runner');
-        expect(option.attr('custom-attr')).toBe('custom-attr');
-      });
-
-      it('should be selected, if it is available and no other option is selected', function() {
-        // selectedIndex is used here because jqLite incorrectly reports element.val()
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}];
-        });
-        createSingleSelect(true);
-        // ensure the first option (the blank option) is selected
-        expect(element[0].selectedIndex).toEqual(0);
-        scope.$digest();
-        // ensure the option has not changed following the digest
-        expect(element[0].selectedIndex).toEqual(0);
-      });
-    });
-
-
-    describe('on change', function() {
-
-      it('should update model on change', function() {
-        createSingleSelect();
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}, {name: 'B'}];
-          scope.selected = scope.values[0];
-        });
-
-        expect(element.val()).toEqual('0');
-
-        element.val('1');
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual(scope.values[1]);
-      });
-
-
-      it('should update model on change through expression', function() {
-        createSelect({
-          'ng-model': 'selected',
-          'ng-options': 'item.id as item.name for item in values'
-        });
-
-        scope.$apply(function() {
-          scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
-          scope.selected = scope.values[0].id;
-        });
-
-        expect(element.val()).toEqual('0');
-
-        element.val('1');
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual(scope.values[1].id);
-      });
-
-
-      it('should update model to null on change', function() {
-        createSingleSelect(true);
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}, {name: 'B'}];
-          scope.selected = scope.values[0];
-          element.val('0');
-        });
-
-        element.val('');
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual(null);
-      });
-
-
-      // Regression https://github.com/angular/angular.js/issues/7855
-      it('should update the model with ng-change', function() {
-        createSelect({
-          'ng-change':'change()',
-          'ng-model':'selected',
-          'ng-options':'value for value in values'
-        });
-
-        scope.$apply(function() {
-          scope.values = ['A', 'B'];
-          scope.selected = 'A';
-        });
-
-        scope.change = function() {
-          scope.selected = 'A';
-        };
-
-        element.find('option')[1].selected = true;
-
-        browserTrigger(element, 'change');
-        expect(element.find('option')[0].selected).toBeTruthy();
-        expect(scope.selected).toEqual('A');
-      });
-    });
-
-    describe('disabled blank', function() {
-      it('should select disabled blank by default', function() {
-        var html = '<select ng-model="someModel" ng-options="c for c in choices">' +
-                     '<option value="" disabled>Choose One</option>' +
-                   '</select>';
-        scope.$apply(function() {
-          scope.choices = ['A', 'B', 'C'];
-        });
-
-        compile(html);
-
-        var options = element.find('option');
-        var optionToSelect = options.eq(0);
-        expect(optionToSelect.text()).toBe('Choose One');
-        expect(optionToSelect.prop('selected')).toBe(true);
-        expect(element[0].value).toBe('');
-
-        dealoc(element);
-      });
-
-
-      it('should select disabled blank by default when select is required', function() {
-        var html = '<select ng-model="someModel" ng-options="c for c in choices" required>' +
-                     '<option value="" disabled>Choose One</option>' +
-                   '</select>';
-        scope.$apply(function() {
-          scope.choices = ['A', 'B', 'C'];
-        });
-
-        compile(html);
-
-        var options = element.find('option');
-        var optionToSelect = options.eq(0);
-        expect(optionToSelect.text()).toBe('Choose One');
-        expect(optionToSelect.prop('selected')).toBe(true);
-        expect(element[0].value).toBe('');
-
-        dealoc(element);
-      });
-    });
-
-    describe('select-many', function() {
-
-      it('should read multiple selection', function() {
-        createMultiSelect();
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}, {name: 'B'}];
-          scope.selected = [];
-        });
-
-        expect(element.find('option').length).toEqual(2);
-        expect(element.find('option')[0].selected).toBeFalsy();
-        expect(element.find('option')[1].selected).toBeFalsy();
-
-        scope.$apply(function() {
-          scope.selected.push(scope.values[1]);
-        });
-
-        expect(element.find('option').length).toEqual(2);
-        expect(element.find('option')[0].selected).toBeFalsy();
-        expect(element.find('option')[1].selected).toBeTruthy();
-
-        scope.$apply(function() {
-          scope.selected.push(scope.values[0]);
-        });
-
-        expect(element.find('option').length).toEqual(2);
-        expect(element.find('option')[0].selected).toBeTruthy();
-        expect(element.find('option')[1].selected).toBeTruthy();
-      });
-
-
-      it('should update model on change', function() {
-        createMultiSelect();
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}, {name: 'B'}];
-          scope.selected = [];
-        });
-
-        element.find('option')[0].selected = true;
-
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual([scope.values[0]]);
-      });
-
-
-      it('should select from object', function() {
-        createSelect({
-          'ng-model':'selected',
-          'multiple':true,
-          'ng-options':'key as value for (key,value) in values'
-        });
-        scope.values = {'0':'A', '1':'B'};
-
-        scope.selected = ['1'];
-        scope.$digest();
-        expect(element.find('option')[1].selected).toBe(true);
-
-        element.find('option')[0].selected = true;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual(['0', '1']);
-
-        element.find('option')[1].selected = false;
-        browserTrigger(element, 'change');
-        expect(scope.selected).toEqual(['0']);
-      });
-
-      it('should deselect all options when model is emptied', function() {
-        createMultiSelect();
-        scope.$apply(function() {
-          scope.values = [{name: 'A'}, {name: 'B'}];
-          scope.selected = [scope.values[0]];
-        });
-        expect(element.find('option')[0].selected).toEqual(true);
-
-        scope.$apply(function() {
-          scope.selected.pop();
-        });
-
-        expect(element.find('option')[0].selected).toEqual(false);
-      });
-    });
-
-
-    describe('ngRequired', function() {
-
-      it('should allow bindings on ngRequired', function() {
-        createSelect({
-          'ng-model': 'value',
-          'ng-options': 'item.name for item in values',
-          'ng-required': 'required'
-        }, true);
-
-
-        scope.$apply(function() {
-          scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}];
-          scope.required = false;
-        });
-
-        element.val('');
-        browserTrigger(element, 'change');
-        expect(element).toBeValid();
-
-        scope.$apply(function() {
-          scope.required = true;
-        });
-        expect(element).toBeInvalid();
-
-        scope.$apply(function() {
-          scope.value = scope.values[0];
-        });
-        expect(element).toBeValid();
-
-        element.val('');
-        browserTrigger(element, 'change');
-        expect(element).toBeInvalid();
-
-        scope.$apply(function() {
-          scope.required = false;
-        });
-        expect(element).toBeValid();
-      });
-
-
-      it('should treat an empty array as invalid when `multiple` attribute used', function() {
-        createSelect({
-          'ng-model': 'value',
-          'ng-options': 'item.name for item in values',
-          'ng-required': 'required',
-          'multiple': ''
-        }, true);
-
-        scope.$apply(function() {
-          scope.value = [];
-          scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}];
-          scope.required = true;
-        });
-        expect(element).toBeInvalid();
-
-        scope.$apply(function() {
-          // ngModelWatch does not set objectEquality flag
-          // array must be replaced in order to trigger $formatters
-          scope.value = [scope.values[0]];
-        });
-        expect(element).toBeValid();
-      });
-
-
-      it('should allow falsy values as values', function() {
-        createSelect({
-          'ng-model': 'value',
-          'ng-options': 'item.value as item.name for item in values',
-          'ng-required': 'required'
-        }, true);
-
-        scope.$apply(function() {
-          scope.values = [{name: 'True', value: true}, {name: 'False', value: false}];
-          scope.required = false;
-        });
-
-        element.val('1');
-        browserTrigger(element, 'change');
-        expect(element).toBeValid();
-        expect(scope.value).toBe(false);
+      expect(element).toBeValid();
+      expect(element).toBePristine();
 
-        scope.$apply(function() {
-          scope.required = true;
-        });
-        expect(element).toBeValid();
-        expect(scope.value).toBe(false);
-      });
+      element[0].value = 'B';
+      browserTrigger(element, 'change');
+      expect(element).toBeValid();
+      expect(element).toBeDirty();
     });
 
-    describe('ngModelCtrl', function() {
-      it('should prefix the model value with the word "the" using $parsers', function() {
-        createSelect({
-          'name': 'select',
-          'ng-model': 'value',
-          'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
-        });
-
-        scope.form.select.$parsers.push(function(value) {
-          return 'the ' + value;
-        });
-
-        element.val('2');
-        browserTrigger(element, 'change');
-        expect(scope.value).toBe('the third');
-        expect(element.val()).toBe('2');
-      });
-
-      it('should prefix the view value with the word "the" using $formatters', function() {
-        createSelect({
-          'name': 'select',
-          'ng-model': 'value',
-          'ng-options': 'item for item in [\'the first\', \'the second\', \'the third\', \'the fourth\']'
-        });
-
-        scope.form.select.$formatters.push(function(value) {
-          return 'the ' + value;
-        });
-
-        scope.$apply(function() {
-          scope.value = 'third';
-        });
-        expect(element.val()).toBe('2');
-      });
-
-      it('should fail validation when $validators fail', function() {
-        createSelect({
-          'name': 'select',
-          'ng-model': 'value',
-          'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
-        });
-
-        scope.form.select.$validators.fail = function() {
-          return false;
-        };
-
-        element.val('2');
-        browserTrigger(element, 'change');
-        expect(element).toBeInvalid();
-        expect(scope.value).toBeUndefined();
-        expect(element.val()).toBe('2');
-      });
-
-      it('should pass validation when $validators pass', function() {
-        createSelect({
-          'name': 'select',
-          'ng-model': 'value',
-          'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
-        });
-
-        scope.form.select.$validators.pass = function() {
-          return true;
-        };
-
-        element.val('2');
-        browserTrigger(element, 'change');
-        expect(element).toBeValid();
-        expect(scope.value).toBe('third');
-        expect(element.val()).toBe('2');
-      });
-
-      it('should fail validation when $asyncValidators fail', inject(function($q, $rootScope) {
-        var defer;
-        createSelect({
-          'name': 'select',
-          'ng-model': 'value',
-          'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
-        });
-
-        scope.form.select.$asyncValidators.async = function() {
-          defer = $q.defer();
-          return defer.promise;
-        };
-
-        element.val('2');
-        browserTrigger(element, 'change');
-        expect(scope.form.select.$pending).toBeDefined();
-        expect(scope.value).toBeUndefined();
-        expect(element.val()).toBe('2');
-
-        defer.reject();
-        $rootScope.$digest();
-        expect(scope.form.select.$pending).toBeUndefined();
-        expect(scope.value).toBeUndefined();
-        expect(element.val()).toBe('2');
-      }));
-
-      it('should pass validation when $asyncValidators pass', inject(function($q, $rootScope) {
-        var defer;
-        createSelect({
-          'name': 'select',
-          'ng-model': 'value',
-          'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']'
-        });
-
-        scope.form.select.$asyncValidators.async = function() {
-          defer = $q.defer();
-          return defer.promise;
-        };
-
-        element.val('2');
-        browserTrigger(element, 'change');
-        expect(scope.form.select.$pending).toBeDefined();
-        expect(scope.value).toBeUndefined();
-        expect(element.val()).toBe('2');
-
-        defer.resolve();
-        $rootScope.$digest();
-        expect(scope.form.select.$pending).toBeUndefined();
-        expect(scope.value).toBe('third');
-        expect(element.val()).toBe('2');
-      }));
-    });
   });
 
 
@@ -2406,12 +831,12 @@ describe('select', function() {
 
     it('should populate value attribute on OPTION', function() {
       compile('<select ng-model="x"><option selected>abc</option></select>');
-      expect(element).toEqualSelect(['? undefined:undefined ?'], 'abc');
+      expect(element).toEqualSelect([unknownValue(undefined)], 'abc');
     });
 
     it('should ignore value if already exists', function() {
       compile('<select ng-model="x"><option value="abc">xyz</option></select>');
-      expect(element).toEqualSelect(['? undefined:undefined ?'], 'abc');
+      expect(element).toEqualSelect([unknownValue(undefined)], 'abc');
     });
 
     it('should set value even if self closing HTML', function() {