function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce,
    $injector) {
  var keyCodes = $mdConstant.KEY_CODE;
  var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW];

  return {
    restrict: 'E',
    require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
    compile: compile,
    controller: function() {
    } // empty placeholder controller to be initialized in link
  };

  function compile(element, attr) {
    // add the select value that will hold our placeholder or selected option value
    var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
    valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
    valueEl.addClass('md-select-value');
    if (!valueEl[0].hasAttribute('id')) {
      valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid());
    }

    // There's got to be an md-content inside. If there's not one, let's add it.
    var mdContentEl = element.find('md-content');
    if (!mdContentEl.length) {
      element.append(angular.element('<md-content>').append(element.contents()));
    }
    mdContentEl.attr('role', 'presentation');


    // Add progress spinner for md-options-loading
    if (attr.mdOnOpen) {

      // Show progress indicator while loading async
      // Use ng-hide for `display:none` so the indicator does not interfere with the options list
      element
        .find('md-content')
        .prepend(angular.element(
          '<div>' +
          ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false" md-diameter="25px"></md-progress-circular>' +
          '</div>'
        ));

      // Hide list [of item options] while loading async
      element
        .find('md-option')
        .attr('ng-show', '$$loadingAsyncDone');
    }

    if (attr.name) {
      var autofillClone = angular.element('<select class="md-visually-hidden"></select>');
      autofillClone.attr({
        'name': attr.name,
        'aria-hidden': 'true',
        'tabindex': '-1'
      });
      var opts = element.find('md-option');
      angular.forEach(opts, function(el) {
        var newEl = angular.element('<option>' + el.innerHTML + '</option>');
        if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
        else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
        autofillClone.append(newEl);
      });

      // Adds an extra option that will hold the selected value for the
      // cases where the select is a part of a non-angular form. This can be done with a ng-model,
      // however if the `md-option` is being `ng-repeat`-ed, AngularJS seems to insert a similar
      // `option` node, but with a value of `? string: <value> ?` which would then get submitted.
      // This also goes around having to prepend a dot to the name attribute.
      autofillClone.append(
        '<option ng-value="' + attr.ngModel + '" selected></option>'
      );

      element.parent().append(autofillClone);
    }

    var isMultiple = $mdUtil.parseAttributeBoolean(attr.multiple);

    // Use everything that's left inside element.contents() as the contents of the menu
    var multipleContent = isMultiple ? 'multiple' : '';
    var selectTemplate = '' +
      '<div class="md-select-menu-container" aria-hidden="true" role="presentation">' +
      '<md-select-menu role="presentation" {0}>{1}</md-select-menu>' +
      '</div>';

    selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, element.html()]);
    element.empty().append(valueEl);
    element.append(selectTemplate);

    if (!attr.tabindex){
      attr.$set('tabindex', 0);
    }

    return function postLink(scope, element, attr, ctrls) {
      var untouched = true;
      var isDisabled, ariaLabelBase;

      var containerCtrl = ctrls[0];
      var mdSelectCtrl = ctrls[1];
      var ngModelCtrl = ctrls[2];
      var formCtrl = ctrls[3];
      // grab a reference to the select menu value label
      var valueEl = element.find('md-select-value');
      var isReadonly = angular.isDefined(attr.readonly);
      var disableAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);

      if (disableAsterisk) {
        element.addClass('md-no-asterisk');
      }

      if (containerCtrl) {
        var isErrorGetter = containerCtrl.isErrorGetter || function() {
          return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
        };

        if (containerCtrl.input) {
          // We ignore inputs that are in the md-select-header (one
          // case where this might be useful would be adding as searchbox)
          if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) {
            throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!");
          }
        }

        containerCtrl.input = element;
        if (!containerCtrl.label) {
          $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
        }

        scope.$watch(isErrorGetter, containerCtrl.setInvalid);
      }

      var selectContainer, selectScope, selectMenuCtrl;

      findSelectContainer();
      $mdTheming(element);

      var originalRender = ngModelCtrl.$render;
      ngModelCtrl.$render = function() {
        originalRender();
        syncLabelText();
        syncAriaLabel();
        inputCheckValue();
      };

      attr.$observe('placeholder', ngModelCtrl.$render);

      if (containerCtrl && containerCtrl.label) {
        attr.$observe('required', function (value) {
          // Toggle the md-required class on the input containers label, because the input container is automatically
          // applying the asterisk indicator on the label.
          containerCtrl.label.toggleClass('md-required', value && !disableAsterisk);
        });
      }

      mdSelectCtrl.setLabelText = function(text) {
        mdSelectCtrl.setIsPlaceholder(!text);

        // Whether the select label has been given via user content rather than the internal
        // template of <md-option>
        var isSelectLabelFromUser = false;

        if (attr.mdSelectedText && attr.mdSelectedHtml) {
          throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`');
        }

        if (attr.mdSelectedText || attr.mdSelectedHtml) {
          text = $parse(attr.mdSelectedText || attr.mdSelectedHtml)(scope);
          isSelectLabelFromUser = true;
        } else if (!text) {
          // Use placeholder attribute, otherwise fallback to the md-input-container label
          var tmpPlaceholder = attr.placeholder ||
              (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');

          text = tmpPlaceholder || '';
          isSelectLabelFromUser = true;
        }

        var target = valueEl.children().eq(0);

        if (attr.mdSelectedHtml) {
            // Using getTrustedHtml will run the content through $sanitize if it is not already
            // explicitly trusted. If the ngSanitize module is not loaded, this will
            // *correctly* throw an sce error.
            target.html($sce.getTrustedHtml(text));
        } else if (isSelectLabelFromUser) {
          target.text(text);
        } else {
          // If we've reached this point, the text is not user-provided.
          target.html(text);
        }
      };

      mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
        if (isPlaceholder) {
          valueEl.addClass('md-select-placeholder');
          if (containerCtrl && containerCtrl.label) {
            containerCtrl.label.addClass('md-placeholder');
          }
        } else {
          valueEl.removeClass('md-select-placeholder');
          if (containerCtrl && containerCtrl.label) {
            containerCtrl.label.removeClass('md-placeholder');
          }
        }
      };

      if (!isReadonly) {
        element
          .on('focus', function(ev) {
            // Always focus the container (if we have one) so floating labels and other styles are
            // applied properly
            containerCtrl && containerCtrl.setFocused(true);
          });

        // Attach before ngModel's blur listener to stop propagation of blur event
        // to prevent from setting $touched.
        element.on('blur', function(event) {
          if (untouched) {
            untouched = false;
            if (selectScope._mdSelectIsOpen) {
              event.stopImmediatePropagation();
            }
          }

          if (selectScope._mdSelectIsOpen) return;
          containerCtrl && containerCtrl.setFocused(false);
          inputCheckValue();
        });
      }

      mdSelectCtrl.triggerClose = function() {
        $parse(attr.mdOnClose)(scope);
      };

      scope.$$postDigest(function() {
        initAriaLabel();
        syncLabelText();
        syncAriaLabel();
      });

      function initAriaLabel() {
        var labelText = element.attr('aria-label') || element.attr('placeholder');
        if (!labelText && containerCtrl && containerCtrl.label) {
          labelText = containerCtrl.label.text();
        }
        ariaLabelBase = labelText;
        $mdAria.expect(element, 'aria-label', labelText);
      }

      scope.$watch(function() {
        return selectMenuCtrl.selectedLabels();
      }, syncLabelText);

      function syncLabelText() {
        if (selectContainer) {
          selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
          mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
        }
      }

      function syncAriaLabel() {
        if (!ariaLabelBase) return;
        var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'});
        element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase);
      }

      var deregisterWatcher;
      attr.$observe('ngMultiple', function(val) {
        if (deregisterWatcher) deregisterWatcher();
        var parser = $parse(val);
        deregisterWatcher = scope.$watch(function() {
          return parser(scope);
        }, function(multiple, prevVal) {
          if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
          if (multiple) {
            element.attr('multiple', 'multiple');
          } else {
            element.removeAttr('multiple');
          }
          element.attr('aria-multiselectable', multiple ? 'true' : 'false');
          if (selectContainer) {
            selectMenuCtrl.setMultiple(multiple);
            originalRender = ngModelCtrl.$render;
            ngModelCtrl.$render = function() {
              originalRender();
              syncLabelText();
              syncAriaLabel();
              inputCheckValue();
            };
            ngModelCtrl.$render();
          }
        });
      });

      attr.$observe('disabled', function(disabled) {
        if (angular.isString(disabled)) {
          disabled = true;
        }
        // Prevent click event being registered twice
        if (isDisabled !== undefined && isDisabled === disabled) {
          return;
        }
        isDisabled = disabled;
        if (disabled) {
          element
            .attr({'aria-disabled': 'true'})
            .removeAttr('tabindex')
            .off('click', openSelect)
            .off('keydown', handleKeypress);
        } else {
          element
            .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'})
            .on('click', openSelect)
            .on('keydown', handleKeypress);
        }
      });

      if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) {
        element.attr({'aria-disabled': 'false'});
        element.on('click', openSelect);
        element.on('keydown', handleKeypress);
      }

      var ariaAttrs = {
        role: 'listbox',
        'aria-expanded': 'false',
        'aria-multiselectable': isMultiple && !attr.ngMultiple ? 'true' : 'false'
      };

      if (!element[0].hasAttribute('id')) {
        ariaAttrs.id = 'select_' + $mdUtil.nextUid();
      }

      var containerId = 'select_container_' + $mdUtil.nextUid();
      selectContainer.attr('id', containerId);
      // Only add aria-owns if element ownership is NOT represented in the DOM.
      if (!element.find('md-select-menu').length) {
        ariaAttrs['aria-owns'] = containerId;
      }
      element.attr(ariaAttrs);

      scope.$on('$destroy', function() {
        $mdSelect
          .destroy()
          .finally(function() {
            if (containerCtrl) {
              containerCtrl.setFocused(false);
              containerCtrl.setHasValue(false);
              containerCtrl.input = null;
            }
            ngModelCtrl.$setTouched();
          });
      });



      function inputCheckValue() {
        // The select counts as having a value if one or more options are selected,
        // or if the input's validity state says it has bad input (eg string in a number input)
        // we must do this on nextTick as the $render is sometimes invoked on nextTick.
        $mdUtil.nextTick(function () {
          containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput);
        });
      }

      function findSelectContainer() {
        selectContainer = angular.element(
          element[0].querySelector('.md-select-menu-container')
        );
        selectScope = scope;
        if (attr.mdContainerClass) {
          var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass;
          selectContainer[0].setAttribute('class', value);
        }
        selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
        selectMenuCtrl.init(ngModelCtrl, attr.ngModel);
        element.on('$destroy', function() {
          selectContainer.remove();
        });
      }

      function handleKeypress(e) {
        if ($mdConstant.isNavigationKey(e)) {
          // prevent page scrolling on interaction
          e.preventDefault();
          openSelect(e);
        } else {
          if (shouldHandleKey(e, $mdConstant)) {
            e.preventDefault();

            var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
            if (!node || node.hasAttribute('disabled')) return;
            var optionCtrl = angular.element(node).controller('mdOption');
            if (!selectMenuCtrl.isMultiple) {
              selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]);
            }
            selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
            selectMenuCtrl.refreshViewValue();
          }
        }
      }

      function openSelect() {
        selectScope._mdSelectIsOpen = true;
        element.attr('aria-expanded', 'true');

        $mdSelect.show({
          scope: selectScope,
          preserveScope: true,
          skipCompile: true,
          element: selectContainer,
          target: element[0],
          selectCtrl: mdSelectCtrl,
          preserveElement: true,
          hasBackdrop: true,
          loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
        }).finally(function() {
          selectScope._mdSelectIsOpen = false;
          element.focus();
          element.attr('aria-expanded', 'false');
          ngModelCtrl.$setTouched();
        });
      }

    };
  }
}
function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce,
    $injector) {
  var keyCodes = $mdConstant.KEY_CODE;
  var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW];

  return {
    restrict: 'E',
    require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
    compile: compile,
    controller: function() {
    } // empty placeholder controller to be initialized in link
  };

  function compile(element, attr) {
    // add the select value that will hold our placeholder or selected option value
    var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
    valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
    valueEl.addClass('md-select-value');
    if (!valueEl[0].hasAttribute('id')) {
      valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid());
    }

    // There's got to be an md-content inside. If there's not one, let's add it.
    var mdContentEl = element.find('md-content');
    if (!mdContentEl.length) {
      element.append(angular.element('<md-content>').append(element.contents()));
    }
    mdContentEl.attr('role', 'presentation');


    // Add progress spinner for md-options-loading
    if (attr.mdOnOpen) {

      // Show progress indicator while loading async
      // Use ng-hide for `display:none` so the indicator does not interfere with the options list
      element
        .find('md-content')
        .prepend(angular.element(
          '<div>' +
          ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false" md-diameter="25px"></md-progress-circular>' +
          '</div>'
        ));

      // Hide list [of item options] while loading async
      element
        .find('md-option')
        .attr('ng-show', '$$loadingAsyncDone');
    }

    if (attr.name) {
      var autofillClone = angular.element('<select class="md-visually-hidden"></select>');
      autofillClone.attr({
        'name': attr.name,
        'aria-hidden': 'true',
        'tabindex': '-1'
      });
      var opts = element.find('md-option');
      angular.forEach(opts, function(el) {
        var newEl = angular.element('<option>' + el.innerHTML + '</option>');
        if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
        else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
        autofillClone.append(newEl);
      });

      // Adds an extra option that will hold the selected value for the
      // cases where the select is a part of a non-angular form. This can be done with a ng-model,
      // however if the `md-option` is being `ng-repeat`-ed, AngularJS seems to insert a similar
      // `option` node, but with a value of `? string: <value> ?` which would then get submitted.
      // This also goes around having to prepend a dot to the name attribute.
      autofillClone.append(
        '<option ng-value="' + attr.ngModel + '" selected></option>'
      );

      element.parent().append(autofillClone);
    }

    var isMultiple = $mdUtil.parseAttributeBoolean(attr.multiple);

    // Use everything that's left inside element.contents() as the contents of the menu
    var multipleContent = isMultiple ? 'multiple' : '';
    var selectTemplate = '' +
      '<div class="md-select-menu-container" aria-hidden="true" role="presentation">' +
      '<md-select-menu role="presentation" {0}>{1}</md-select-menu>' +
      '</div>';

    selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, element.html()]);
    element.empty().append(valueEl);
    element.append(selectTemplate);

    if (!attr.tabindex){
      attr.$set('tabindex', 0);
    }

    return function postLink(scope, element, attr, ctrls) {
      var untouched = true;
      var isDisabled, ariaLabelBase;

      var containerCtrl = ctrls[0];
      var mdSelectCtrl = ctrls[1];
      var ngModelCtrl = ctrls[2];
      var formCtrl = ctrls[3];
      // grab a reference to the select menu value label
      var valueEl = element.find('md-select-value');
      var isReadonly = angular.isDefined(attr.readonly);
      var disableAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);

      if (disableAsterisk) {
        element.addClass('md-no-asterisk');
      }

      if (containerCtrl) {
        var isErrorGetter = containerCtrl.isErrorGetter || function() {
          return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
        };

        if (containerCtrl.input) {
          // We ignore inputs that are in the md-select-header (one
          // case where this might be useful would be adding as searchbox)
          if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) {
            throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!");
          }
        }

        containerCtrl.input = element;
        if (!containerCtrl.label) {
          $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
        }

        scope.$watch(isErrorGetter, containerCtrl.setInvalid);
      }

      var selectContainer, selectScope, selectMenuCtrl;

      findSelectContainer();
      $mdTheming(element);

      var originalRender = ngModelCtrl.$render;
      ngModelCtrl.$render = function() {
        originalRender();
        syncLabelText();
        syncAriaLabel();
        inputCheckValue();
      };

      attr.$observe('placeholder', ngModelCtrl.$render);

      if (containerCtrl && containerCtrl.label) {
        attr.$observe('required', function (value) {
          // Toggle the md-required class on the input containers label, because the input container is automatically
          // applying the asterisk indicator on the label.
          containerCtrl.label.toggleClass('md-required', value && !disableAsterisk);
        });
      }

      mdSelectCtrl.setLabelText = function(text) {
        mdSelectCtrl.setIsPlaceholder(!text);

        // Whether the select label has been given via user content rather than the internal
        // template of <md-option>
        var isSelectLabelFromUser = false;

        if (attr.mdSelectedText && attr.mdSelectedHtml) {
          throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`');
        }

        if (attr.mdSelectedText || attr.mdSelectedHtml) {
          text = $parse(attr.mdSelectedText || attr.mdSelectedHtml)(scope);
          isSelectLabelFromUser = true;
        } else if (!text) {
          // Use placeholder attribute, otherwise fallback to the md-input-container label
          var tmpPlaceholder = attr.placeholder ||
              (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');

          text = tmpPlaceholder || '';
          isSelectLabelFromUser = true;
        }

        var target = valueEl.children().eq(0);

        if (attr.mdSelectedHtml) {
            // Using getTrustedHtml will run the content through $sanitize if it is not already
            // explicitly trusted. If the ngSanitize module is not loaded, this will
            // *correctly* throw an sce error.
            target.html($sce.getTrustedHtml(text));
        } else if (isSelectLabelFromUser) {
          target.text(text);
        } else {
          // If we've reached this point, the text is not user-provided.
          target.html(text);
        }
      };

      mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
        if (isPlaceholder) {
          valueEl.addClass('md-select-placeholder');
          if (containerCtrl && containerCtrl.label) {
            containerCtrl.label.addClass('md-placeholder');
          }
        } else {
          valueEl.removeClass('md-select-placeholder');
          if (containerCtrl && containerCtrl.label) {
            containerCtrl.label.removeClass('md-placeholder');
          }
        }
      };

      if (!isReadonly) {
        element
          .on('focus', function(ev) {
            // Always focus the container (if we have one) so floating labels and other styles are
            // applied properly
            containerCtrl && containerCtrl.setFocused(true);
          });

        // Attach before ngModel's blur listener to stop propagation of blur event
        // to prevent from setting $touched.
        element.on('blur', function(event) {
          if (untouched) {
            untouched = false;
            if (selectScope._mdSelectIsOpen) {
              event.stopImmediatePropagation();
            }
          }

          if (selectScope._mdSelectIsOpen) return;
          containerCtrl && containerCtrl.setFocused(false);
          inputCheckValue();
        });
      }

      mdSelectCtrl.triggerClose = function() {
        $parse(attr.mdOnClose)(scope);
      };

      scope.$$postDigest(function() {
        initAriaLabel();
        syncLabelText();
        syncAriaLabel();
      });

      function initAriaLabel() {
        var labelText = element.attr('aria-label') || element.attr('placeholder');
        if (!labelText && containerCtrl && containerCtrl.label) {
          labelText = containerCtrl.label.text();
        }
        ariaLabelBase = labelText;
        $mdAria.expect(element, 'aria-label', labelText);
      }

      scope.$watch(function() {
        return selectMenuCtrl.selectedLabels();
      }, syncLabelText);

      function syncLabelText() {
        if (selectContainer) {
          selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
          mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
        }
      }

      function syncAriaLabel() {
        if (!ariaLabelBase) return;
        var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'});
        element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase);
      }

      var deregisterWatcher;
      attr.$observe('ngMultiple', function(val) {
        if (deregisterWatcher) deregisterWatcher();
        var parser = $parse(val);
        deregisterWatcher = scope.$watch(function() {
          return parser(scope);
        }, function(multiple, prevVal) {
          if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
          if (multiple) {
            element.attr('multiple', 'multiple');
          } else {
            element.removeAttr('multiple');
          }
          element.attr('aria-multiselectable', multiple ? 'true' : 'false');
          if (selectContainer) {
            selectMenuCtrl.setMultiple(multiple);
            originalRender = ngModelCtrl.$render;
            ngModelCtrl.$render = function() {
              originalRender();
              syncLabelText();
              syncAriaLabel();
              inputCheckValue();
            };
            ngModelCtrl.$render();
          }
        });
      });

      attr.$observe('disabled', function(disabled) {
        if (angular.isString(disabled)) {
          disabled = true;
        }
        // Prevent click event being registered twice
        if (isDisabled !== undefined && isDisabled === disabled) {
          return;
        }
        isDisabled = disabled;
        if (disabled) {
          element
            .attr({'aria-disabled': 'true'})
            .removeAttr('tabindex')
            .off('click', openSelect)
            .off('keydown', handleKeypress);
        } else {
          element
            .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'})
            .on('click', openSelect)
            .on('keydown', handleKeypress);
        }
      });

      if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) {
        element.attr({'aria-disabled': 'false'});
        element.on('click', openSelect);
        element.on('keydown', handleKeypress);
      }

      var ariaAttrs = {
        role: 'listbox',
        'aria-expanded': 'false',
        'aria-multiselectable': isMultiple && !attr.ngMultiple ? 'true' : 'false'
      };

      if (!element[0].hasAttribute('id')) {
        ariaAttrs.id = 'select_' + $mdUtil.nextUid();
      }

      var containerId = 'select_container_' + $mdUtil.nextUid();
      selectContainer.attr('id', containerId);
      // Only add aria-owns if element ownership is NOT represented in the DOM.
      if (!element.find('md-select-menu').length) {
        ariaAttrs['aria-owns'] = containerId;
      }
      element.attr(ariaAttrs);

      scope.$on('$destroy', function() {
        $mdSelect
          .destroy()
          .finally(function() {
            if (containerCtrl) {
              containerCtrl.setFocused(false);
              containerCtrl.setHasValue(false);
              containerCtrl.input = null;
            }
            ngModelCtrl.$setTouched();
          });
      });



      function inputCheckValue() {
        // The select counts as having a value if one or more options are selected,
        // or if the input's validity state says it has bad input (eg string in a number input)
        // we must do this on nextTick as the $render is sometimes invoked on nextTick.
        $mdUtil.nextTick(function () {
          containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput);
        });
      }

      function findSelectContainer() {
        selectContainer = angular.element(
          element[0].querySelector('.md-select-menu-container')
        );
        selectScope = scope;
        if (attr.mdContainerClass) {
          var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass;
          selectContainer[0].setAttribute('class', value);
        }
        selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
        selectMenuCtrl.init(ngModelCtrl, attr.ngModel);
        element.on('$destroy', function() {
          selectContainer.remove();
        });
      }

      function handleKeypress(e) {
        if ($mdConstant.isNavigationKey(e)) {
          // prevent page scrolling on interaction
          e.preventDefault();
          openSelect(e);
        } else {
          if (shouldHandleKey(e, $mdConstant)) {
            e.preventDefault();

            var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
            if (!node || node.hasAttribute('disabled')) return;
            var optionCtrl = angular.element(node).controller('mdOption');
            if (!selectMenuCtrl.isMultiple) {
              selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]);
            }
            selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
            selectMenuCtrl.refreshViewValue();
          }
        }
      }

      function openSelect() {
        selectScope._mdSelectIsOpen = true;
        element.attr('aria-expanded', 'true');

        $mdSelect.show({
          scope: selectScope,
          preserveScope: true,
          skipCompile: true,
          element: selectContainer,
          target: element[0],
          selectCtrl: mdSelectCtrl,
          preserveElement: true,
          hasBackdrop: true,
          loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
        }).finally(function() {
          selectScope._mdSelectIsOpen = false;
          element.focus();
          element.attr('aria-expanded', 'false');
          ngModelCtrl.$setTouched();
        });
      }

    };
  }
}
function SelectDirective ( $mdSelect , $mdUtil , $mdConstant , $mdTheming , $mdAria , $parse , $sce , $injector ) { var keyCodes = $mdConstant . KEY_CODE ; var NAVIGATION_KEYS = [ keyCodes . SPACE , keyCodes . ENTER , keyCodes . UP_ARROW , keyCodes . DOWN_ARROW ] ; return { restrict : 'E' , require : [ '^?mdInputContainer' , 'mdSelect' , 'ngModel' , '?^form' ] , compile : compile , controller : function ( ) { } // empty placeholder controller to be initialized in link } ; function compile ( element , attr ) { // add the select value that will hold our placeholder or selected option value var valueEl = angular . element ( '<md-select-value><span></span></md-select-value>' ) ; valueEl . append ( '<span class="md-select-icon" aria-hidden="true"></span>' ) ; valueEl . addClass ( 'md-select-value' ) ; if ( ! valueEl [ 0 ] . hasAttribute ( 'id' ) ) { valueEl . attr ( 'id' , 'select_value_label_' + $mdUtil . nextUid ( ) ) ; } // There's got to be an md-content inside. If there's not one, let's add it. var mdContentEl = element . find ( 'md-content' ) ; if ( ! mdContentEl . length ) { element . append ( angular . element ( '<md-content>' ) . append ( element . contents ( ) ) ) ; } mdContentEl . attr ( 'role' , 'presentation' ) ; // Add progress spinner for md-options-loading if ( attr . mdOnOpen ) { // Show progress indicator while loading async // Use ng-hide for `display:none` so the indicator does not interfere with the options list element . find ( 'md-content' ) . prepend ( angular . element ( '<div>' + ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false" md-diameter="25px"></md-progress-circular>' + '</div>' ) ) ; // Hide list [of item options] while loading async element . find ( 'md-option' ) . attr ( 'ng-show' , '$$loadingAsyncDone' ) ; } if ( attr . name ) { var autofillClone = angular . element ( '<select class="md-visually-hidden"></select>' ) ; autofillClone . attr ( { 'name' : attr . name , 'aria-hidden' : 'true' , 'tabindex' : '-1' } ) ; var opts = element . find ( 'md-option' ) ; angular . forEach ( opts , function ( el ) { var newEl = angular . element ( '<option>' + el . innerHTML + '</option>' ) ; if ( el . hasAttribute ( 'ng-value' ) ) newEl . attr ( 'ng-value' , el . getAttribute ( 'ng-value' ) ) ; else if ( el . hasAttribute ( 'value' ) ) newEl . attr ( 'value' , el . getAttribute ( 'value' ) ) ; autofillClone . append ( newEl ) ; } ) ; // Adds an extra option that will hold the selected value for the // cases where the select is a part of a non-angular form. This can be done with a ng-model, // however if the `md-option` is being `ng-repeat`-ed, AngularJS seems to insert a similar // `option` node, but with a value of `? string: <value> ?` which would then get submitted. // This also goes around having to prepend a dot to the name attribute. autofillClone . append ( '<option ng-value="' + attr . ngModel + '" selected></option>' ) ; element . parent ( ) . append ( autofillClone ) ; } var isMultiple = $mdUtil . parseAttributeBoolean ( attr . multiple ) ; // Use everything that's left inside element.contents() as the contents of the menu var multipleContent = isMultiple ? 'multiple' : '' ; var selectTemplate = '' + '<div class="md-select-menu-container" aria-hidden="true" role="presentation">' + '<md-select-menu role="presentation" {0}>{1}</md-select-menu>' + '</div>' ; selectTemplate = $mdUtil . supplant ( selectTemplate , [ multipleContent , element . html ( ) ] ) ; element . empty ( ) . append ( valueEl ) ; element . append ( selectTemplate ) ; if ( ! attr . tabindex ) { attr . $set ( 'tabindex' , 0 ) ; } return function postLink ( scope , element , attr , ctrls ) { var untouched = true ; var isDisabled , ariaLabelBase ; var containerCtrl = ctrls [ 0 ] ; var mdSelectCtrl = ctrls [ 1 ] ; var ngModelCtrl = ctrls [ 2 ] ; var formCtrl = ctrls [ 3 ] ; // grab a reference to the select menu value label var valueEl = element . find ( 'md-select-value' ) ; var isReadonly = angular . isDefined ( attr . readonly ) ; var disableAsterisk = $mdUtil . parseAttributeBoolean ( attr . mdNoAsterisk ) ; if ( disableAsterisk ) { element . addClass ( 'md-no-asterisk' ) ; } if ( containerCtrl ) { var isErrorGetter = containerCtrl . isErrorGetter || function ( ) { return ngModelCtrl . $invalid && ( ngModelCtrl . $touched || ( formCtrl && formCtrl . $submitted ) ) ; } ; if ( containerCtrl . input ) { // We ignore inputs that are in the md-select-header (one // case where this might be useful would be adding as searchbox) if ( element . find ( 'md-select-header' ) . find ( 'input' ) [ 0 ] !== containerCtrl . input [ 0 ] ) { throw new Error ( "<md-input-container> can only have *one* child <input>, <textarea> or <select> element!" ) ; } } containerCtrl . input = element ; if ( ! containerCtrl . label ) { $mdAria . expect ( element , 'aria-label' , element . attr ( 'placeholder' ) ) ; } scope . $watch ( isErrorGetter , containerCtrl . setInvalid ) ; } var selectContainer , selectScope , selectMenuCtrl ; findSelectContainer ( ) ; $mdTheming ( element ) ; var originalRender = ngModelCtrl . $render ; ngModelCtrl . $render = function ( ) { originalRender ( ) ; syncLabelText ( ) ; syncAriaLabel ( ) ; inputCheckValue ( ) ; } ; attr . $observe ( 'placeholder' , ngModelCtrl . $render ) ; if ( containerCtrl && containerCtrl . label ) { attr . $observe ( 'required' , function ( value ) { // Toggle the md-required class on the input containers label, because the input container is automatically // applying the asterisk indicator on the label. containerCtrl . label . toggleClass ( 'md-required' , value && ! disableAsterisk ) ; } ) ; } mdSelectCtrl . setLabelText = function ( text ) { mdSelectCtrl . setIsPlaceholder ( ! text ) ; // Whether the select label has been given via user content rather than the internal // template of <md-option> var isSelectLabelFromUser = false ; if ( attr . mdSelectedText && attr . mdSelectedHtml ) { throw Error ( 'md-select cannot have both `md-selected-text` and `md-selected-html`' ) ; } if ( attr . mdSelectedText || attr . mdSelectedHtml ) { text = $parse ( attr . mdSelectedText || attr . mdSelectedHtml ) ( scope ) ; isSelectLabelFromUser = true ; } else if ( ! text ) { // Use placeholder attribute, otherwise fallback to the md-input-container label var tmpPlaceholder = attr . placeholder || ( containerCtrl && containerCtrl . label ? containerCtrl . label . text ( ) : '' ) ; text = tmpPlaceholder || '' ; isSelectLabelFromUser = true ; } var target = valueEl . children ( ) . eq ( 0 ) ; if ( attr . mdSelectedHtml ) { // Using getTrustedHtml will run the content through $sanitize if it is not already // explicitly trusted. If the ngSanitize module is not loaded, this will // *correctly* throw an sce error. target . html ( $sce . getTrustedHtml ( text ) ) ; } else if ( isSelectLabelFromUser ) { target . text ( text ) ; } else { // If we've reached this point, the text is not user-provided. target . html ( text ) ; } } ; mdSelectCtrl . setIsPlaceholder = function ( isPlaceholder ) { if ( isPlaceholder ) { valueEl . addClass ( 'md-select-placeholder' ) ; if ( containerCtrl && containerCtrl . label ) { containerCtrl . label . addClass ( 'md-placeholder' ) ; } } else { valueEl . removeClass ( 'md-select-placeholder' ) ; if ( containerCtrl && containerCtrl . label ) { containerCtrl . label . removeClass ( 'md-placeholder' ) ; } } } ; if ( ! isReadonly ) { element . on ( 'focus' , function ( ev ) { // Always focus the container (if we have one) so floating labels and other styles are // applied properly containerCtrl && containerCtrl . setFocused ( true ) ; } ) ; // Attach before ngModel's blur listener to stop propagation of blur event // to prevent from setting $touched. element . on ( 'blur' , function ( event ) { if ( untouched ) { untouched = false ; if ( selectScope . _mdSelectIsOpen ) { event . stopImmediatePropagation ( ) ; } } if ( selectScope . _mdSelectIsOpen ) return ; containerCtrl && containerCtrl . setFocused ( false ) ; inputCheckValue ( ) ; } ) ; } mdSelectCtrl . triggerClose = function ( ) { $parse ( attr . mdOnClose ) ( scope ) ; } ; scope . $$postDigest ( function ( ) { initAriaLabel ( ) ; syncLabelText ( ) ; syncAriaLabel ( ) ; } ) ; function initAriaLabel ( ) { var labelText = element . attr ( 'aria-label' ) || element . attr ( 'placeholder' ) ; if ( ! labelText && containerCtrl && containerCtrl . label ) { labelText = containerCtrl . label . text ( ) ; } ariaLabelBase = labelText ; $mdAria . expect ( element , 'aria-label' , labelText ) ; } scope . $watch ( function ( ) { return selectMenuCtrl . selectedLabels ( ) ; } , syncLabelText ) ; function syncLabelText ( ) { if ( selectContainer ) { selectMenuCtrl = selectMenuCtrl || selectContainer . find ( 'md-select-menu' ) . controller ( 'mdSelectMenu' ) ; mdSelectCtrl . setLabelText ( selectMenuCtrl . selectedLabels ( ) ) ; } } function syncAriaLabel ( ) { if ( ! ariaLabelBase ) return ; var ariaLabels = selectMenuCtrl . selectedLabels ( { mode : 'aria' } ) ; element . attr ( 'aria-label' , ariaLabels . length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase ) ; } var deregisterWatcher ; attr . $observe ( 'ngMultiple' , function ( val ) { if ( deregisterWatcher ) deregisterWatcher ( ) ; var parser = $parse ( val ) ; deregisterWatcher = scope . $watch ( function ( ) { return parser ( scope ) ; } , function ( multiple , prevVal ) { if ( multiple === undefined && prevVal === undefined ) return ; // assume compiler did a good job if ( multiple ) { element . attr ( 'multiple' , 'multiple' ) ; } else { element . removeAttr ( 'multiple' ) ; } element . attr ( 'aria-multiselectable' , multiple ? 'true' : 'false' ) ; if ( selectContainer ) { selectMenuCtrl . setMultiple ( multiple ) ; originalRender = ngModelCtrl . $render ; ngModelCtrl . $render = function ( ) { originalRender ( ) ; syncLabelText ( ) ; syncAriaLabel ( ) ; inputCheckValue ( ) ; } ; ngModelCtrl . $render ( ) ; } } ) ; } ) ; attr . $observe ( 'disabled' , function ( disabled ) { if ( angular . isString ( disabled ) ) { disabled = true ; } // Prevent click event being registered twice if ( isDisabled !== undefined && isDisabled === disabled ) { return ; } isDisabled = disabled ; if ( disabled ) { element . attr ( { 'aria-disabled' : 'true' } ) . removeAttr ( 'tabindex' ) . off ( 'click' , openSelect ) . off ( 'keydown' , handleKeypress ) ; } else { element . attr ( { 'tabindex' : attr . tabindex , 'aria-disabled' : 'false' } ) . on ( 'click' , openSelect ) . on ( 'keydown' , handleKeypress ) ; } } ) ; if ( ! attr . hasOwnProperty ( 'disabled' ) && ! attr . hasOwnProperty ( 'ngDisabled' ) ) { element . attr ( { 'aria-disabled' : 'false' } ) ; element . on ( 'click' , openSelect ) ; element . on ( 'keydown' , handleKeypress ) ; } var ariaAttrs = { role : 'listbox' , 'aria-expanded' : 'false' , 'aria-multiselectable' : isMultiple && ! attr . ngMultiple ? 'true' : 'false' } ; if ( ! element [ 0 ] . hasAttribute ( 'id' ) ) { ariaAttrs . id = 'select_' + $mdUtil . nextUid ( ) ; } var containerId = 'select_container_' + $mdUtil . nextUid ( ) ; selectContainer . attr ( 'id' , containerId ) ; // Only add aria-owns if element ownership is NOT represented in the DOM. if ( ! element . find ( 'md-select-menu' ) . length ) { ariaAttrs [ 'aria-owns' ] = containerId ; } element . attr ( ariaAttrs ) ; scope . $on ( '$destroy' , function ( ) { $mdSelect . destroy ( ) . finally ( function ( ) { if ( containerCtrl ) { containerCtrl . setFocused ( false ) ; containerCtrl . setHasValue ( false ) ; containerCtrl . input = null ; } ngModelCtrl . $setTouched ( ) ; } ) ; } ) ; function inputCheckValue ( ) { // The select counts as having a value if one or more options are selected, // or if the input's validity state says it has bad input (eg string in a number input) // we must do this on nextTick as the $render is sometimes invoked on nextTick. $mdUtil . nextTick ( function ( ) { containerCtrl && containerCtrl . setHasValue ( selectMenuCtrl . selectedLabels ( ) . length > 0 || ( element [ 0 ] . validity || { } ) . badInput ) ; } ) ; } function findSelectContainer ( ) { selectContainer = angular . element ( element [ 0 ] . querySelector ( '.md-select-menu-container' ) ) ; selectScope = scope ; if ( attr . mdContainerClass ) { var value = selectContainer [ 0 ] . getAttribute ( 'class' ) + ' ' + attr . mdContainerClass ; selectContainer [ 0 ] . setAttribute ( 'class' , value ) ; } selectMenuCtrl = selectContainer . find ( 'md-select-menu' ) . controller ( 'mdSelectMenu' ) ; selectMenuCtrl . init ( ngModelCtrl , attr . ngModel ) ; element . on ( '$destroy' , function ( ) { selectContainer . remove ( ) ; } ) ; } function handleKeypress ( e ) { if ( $mdConstant . isNavigationKey ( e ) ) { // prevent page scrolling on interaction e . preventDefault ( ) ; openSelect ( e ) ; } else { if ( shouldHandleKey ( e , $mdConstant ) ) { e . preventDefault ( ) ; var node = selectMenuCtrl . optNodeForKeyboardSearch ( e ) ; if ( ! node || node . hasAttribute ( 'disabled' ) ) return ; var optionCtrl = angular . element ( node ) . controller ( 'mdOption' ) ; if ( ! selectMenuCtrl . isMultiple ) { selectMenuCtrl . deselect ( Object . keys ( selectMenuCtrl . selected ) [ 0 ] ) ; } selectMenuCtrl . select ( optionCtrl . hashKey , optionCtrl . value ) ; selectMenuCtrl . refreshViewValue ( ) ; } } } function openSelect ( ) { selectScope . _mdSelectIsOpen = true ; element . attr ( 'aria-expanded' , 'true' ) ; $mdSelect . show ( { scope : selectScope , preserveScope : true , skipCompile : true , element : selectContainer , target : element [ 0 ] , selectCtrl : mdSelectCtrl , preserveElement : true , hasBackdrop : true , loadingAsync : attr . mdOnOpen ? scope . $eval ( attr . mdOnOpen ) || true : false } ) . finally ( function ( ) { selectScope . _mdSelectIsOpen = false ; element . focus ( ) ; element . attr ( 'aria-expanded' , 'false' ) ; ngModelCtrl . $setTouched ( ) ; } ) ; } } ; } }
@ngdoc directive
@name mdSelect
@restrict E
@module material.components.select

@description Displays a select box, bound to an `ng-model`. Selectable options are defined using
the <a ng-href="/api/directive/mdOption">md-option</a> element directive. Options can be grouped
using the <a ng-href="/api/directive/mdOptgroup">md-optgroup</a> element directive.

When the select is required and uses a floating label, then the label will automatically contain
an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute.

By default, the select will display with an underline to match other form elements. This can be
disabled by applying the `md-no-underline` CSS class.

@param {expression} ng-model Assignable angular expression to data-bind to.
@param {expression=} ng-change Expression to be executed when the model value changes.
@param {boolean=} multiple When present, allows for more than one option to be selected.
The model is an array with the selected choices. **Note:** This attribute is only evaluated
once; it is not watched.
@param {expression=} md-on-close Expression to be evaluated when the select is closed.
@param {expression=} md-on-open Expression to be evaluated when opening the select.
Will hide the select options and show a spinner until the evaluated promise resolves.
@param {expression=} md-selected-text Expression to be evaluated that will return a string
to be displayed as a placeholder in the select input box when it is closed. The value
will be treated as *text* (not html).
@param {expression=} md-selected-html Expression to be evaluated that will return a string
to be displayed as a placeholder in the select input box when it is closed. The value
will be treated as *html*. The value must either be explicitly marked as trustedHtml or
the ngSanitize module must be loaded.
@param {string=} placeholder Placeholder hint text.
@param {boolean=} md-no-asterisk When set to true, an asterisk will not be appended to the
floating label. **Note:** This attribute is only evaluated once; it is not watched.
@param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or
explicit label is present.
@param {string=} md-container-class Class list to get applied to the `.md-select-menu-container`
element (for custom styling).

@usage
With a placeholder (label and aria-label are added dynamically)
<hljs lang="html">
<md-input-container>
<md-select
ng-model="someModel"
placeholder="Select a state">
<md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
</md-select>
</md-input-container>
</hljs>

With an explicit label
<hljs lang="html">
<md-input-container>
<label>State</label>
<md-select
ng-model="someModel">
<md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
</md-select>
</md-input-container>
</hljs>

Using the `md-select-header` element directive

When a developer needs to put more than just a text label in the `md-select-menu`, they should
use one or more `md-select-header`s. These elements can contain custom HTML which can be styled
as desired. Use cases for this element include a sticky search bar and custom option group
labels.

<hljs lang="html">
<md-input-container>
<md-select ng-model="someModel">
<md-select-header>
<span> Neighborhoods - </span>
</md-select-header>
<md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
</md-select>
</md-input-container>
</hljs>

## Selects and object equality
When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles
equality. Consider the following example:
<hljs lang="js">
angular.controller('MyCtrl', function($scope) {
$scope.users = [
{ id: 1, name: 'Bob' },
{ id: 2, name: 'Alice' },
{ id: 3, name: 'Steve' }
];
$scope.selectedUser = { id: 1, name: 'Bob' };
});
</hljs>
<hljs lang="html">
<div ng-controller="MyCtrl">
<md-select ng-model="selectedUser">
<md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
</md-select>
</div>
</hljs>

At first one might expect that the select should be populated with "Bob" as the selected user.
However, this is not true. To determine whether something is selected,
`ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`;

Javascript's `==` operator does not check for deep equality (ie. that all properties
on the object are the same), but instead whether the objects are *the same object in memory*.
In this case, we have two instances of identical objects, but they exist in memory as unique
entities. Because of this, the select will have no value populated for a selected user.

To get around this, `ngModelController` provides a `track by` option that allows us to specify a
different expression which will be used for the equality operator. As such, we can update our
`html` to make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the
`md-select` element. This converts our equality expression to be
`$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));`
which results in Bob being selected as desired.

**Note:** We do not support AngularJS's `track by` syntax. For instance
`ng-options="user in users track by user.id"` will not work with `md-select`.

Working HTML:
<hljs lang="html">
<div ng-controller="MyCtrl">
<md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}">
<md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
</md-select>
</div>
</hljs>
@ngdoc directive @name mdSelect @restrict E @module material . components . select
Select directive.