/
githubmirror
/
angular.js
Обзор
Документация
Войти
/
githubmirror
/
angular.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/ng/directive/select.js
897 строк
33 KB
Martin Staffa
fix(select): allow to select first option with value `undefined`
06 авг 2018, 13:47
Не верифицирован
06 авг 2018, 13:47
79ca0f1
Код
Авторство
О чём код?
'use strict'; /* exported selectDirective, optionDirective */ var noopNgModelController = { $setViewValue: noop, $render: noop }; function setOptionSelectedStatus(optionEl, value) { optionEl.prop('selected', value); /** * When unselecting an option, setting the property to null / false should be enough * However, screenreaders might react to the selected attribute instead, see * https://github.com/angular/angular.js/issues/14419 * Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false * or null */ optionEl.attr('selected', value); } /** * @ngdoc type * @name select.SelectController * * @description * The controller for the {@link ng.select select} directive. The controller exposes * a few utility methods that can be used to augment the behavior of a regular or an * {@link ng.ngOptions ngOptions} select element. * * @example * ### Set a custom error when the unknown option is selected * * This example sets a custom error "unknownValue" on the ngModelController * when the select element's unknown option is selected, i.e. when the model is set to a value * that is not matched by any option. * * <example name="select-unknown-value-error" module="staticSelect"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="myForm"> * <label for="testSelect"> Single select: </label><br> * <select name="testSelect" ng-model="selected" unknown-value-error> * <option value="option-1">Option 1</option> * <option value="option-2">Option 2</option> * </select><br> * <span class="error" ng-if="myForm.testSelect.$error.unknownValue"> * Error: The current model doesn't match any option</span><br> * * <button ng-click="forceUnknownOption()">Force unknown option</button><br> * </form> * </div> * </file> * <file name="app.js"> * angular.module('staticSelect', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.selected = null; * * $scope.forceUnknownOption = function() { * $scope.selected = 'nonsense'; * }; * }]) * .directive('unknownValueError', function() { * return { * require: ['ngModel', 'select'], * link: function(scope, element, attrs, ctrls) { * var ngModelCtrl = ctrls[0]; * var selectCtrl = ctrls[1]; * * ngModelCtrl.$validators.unknownValue = function(modelValue, viewValue) { * if (selectCtrl.$isUnknownOptionSelected()) { * return false; * } * * return true; * }; * } * * }; * }); * </file> *</example> * * * @example * ### Set the "required" error when the unknown option is selected. * * By default, the "required" error on the ngModelController is only set on a required select * when the empty option is selected. This example adds a custom directive that also sets the * error when the unknown option is selected. * * <example name="select-unknown-value-required" module="staticSelect"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="myForm"> * <label for="testSelect"> Select: </label><br> * <select name="testSelect" ng-model="selected" required unknown-value-required> * <option value="option-1">Option 1</option> * <option value="option-2">Option 2</option> * </select><br> * <span class="error" ng-if="myForm.testSelect.$error.required">Error: Please select a value</span><br> * * <button ng-click="forceUnknownOption()">Force unknown option</button><br> * </form> * </div> * </file> * <file name="app.js"> * angular.module('staticSelect', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.selected = null; * * $scope.forceUnknownOption = function() { * $scope.selected = 'nonsense'; * }; * }]) * .directive('unknownValueRequired', function() { * return { * priority: 1, // This directive must run after the required directive has added its validator * require: ['ngModel', 'select'], * link: function(scope, element, attrs, ctrls) { * var ngModelCtrl = ctrls[0]; * var selectCtrl = ctrls[1]; * * var originalRequiredValidator = ngModelCtrl.$validators.required; * * ngModelCtrl.$validators.required = function() { * if (attrs.required && selectCtrl.$isUnknownOptionSelected()) { * return false; * } * * return originalRequiredValidator.apply(this, arguments); * }; * } * }; * }); * </file> * <file name="protractor.js" type="protractor"> * it('should show the error message when the unknown option is selected', function() { var error = element(by.className('error')); expect(error.getText()).toBe('Error: Please select a value'); element(by.cssContainingText('option', 'Option 1')).click(); expect(error.isPresent()).toBe(false); element(by.tagName('button')).click(); expect(error.getText()).toBe('Error: Please select a value'); }); * </file> *</example> * * */ var SelectController = ['$element', '$scope', /** @this */ function($element, $scope) { var self = this, optionsMap = new NgMap(); self.selectValueMap = {}; // Keys are the hashed values, values the original values // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors self.ngModelCtrl = noopNgModelController; self.multiple = false; // 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. // // Support: IE 9 only // We can't just jqLite('<option>') since jqLite is not smart enough // to create it in <select> and IE barfs otherwise. self.unknownOption = jqLite(window.document.createElement('option')); // The empty option is an option with the value '' that the application developer can // provide inside the select. It is always selectable and indicates that a "null" selection has // been made by the user. // If the select has an empty option, and the model of the select is set to "undefined" or "null", // the empty option is selected. // If the model is set to a different unmatched value, the unknown option is rendered and // selected, i.e both are present, because a "null" selection and an unknown value are different. self.hasEmptyOption = false; self.emptyOption = undefined; self.renderUnknownOption = function(val) { var unknownVal = self.generateUnknownOptionValue(val); self.unknownOption.val(unknownVal); $element.prepend(self.unknownOption); setOptionSelectedStatus(self.unknownOption, true); $element.val(unknownVal); }; self.updateUnknownOption = function(val) { var unknownVal = self.generateUnknownOptionValue(val); self.unknownOption.val(unknownVal); setOptionSelectedStatus(self.unknownOption, true); $element.val(unknownVal); }; self.generateUnknownOptionValue = function(val) { return '? ' + hashKey(val) + ' ?'; }; self.removeUnknownOption = function() { if (self.unknownOption.parent()) self.unknownOption.remove(); }; self.selectEmptyOption = function() { if (self.emptyOption) { $element.val(''); setOptionSelectedStatus(self.emptyOption, true); } }; self.unselectEmptyOption = function() { if (self.hasEmptyOption) { setOptionSelectedStatus(self.emptyOption, false); } }; $scope.$on('$destroy', function() { // disable unknown option so that we don't do work when the whole select is being destroyed self.renderUnknownOption = noop; }); // 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() { var val = $element.val(); // ngValue added option values are stored in the selectValueMap, normal interpolations are not var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val; if (self.hasOption(realVal)) { return realVal; } return null; }; // 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) { // Make sure to remove the selected attribute from the previously selected option // Otherwise, screen readers might get confused var currentlySelectedOption = $element[0].options[$element[0].selectedIndex]; if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false); if (self.hasOption(value)) { self.removeUnknownOption(); var hashedVal = hashKey(value); $element.val(hashedVal in self.selectValueMap ? hashedVal : value); // Set selected attribute and property on selected option for screen readers var selectedOption = $element[0].options[$element[0].selectedIndex]; setOptionSelectedStatus(jqLite(selectedOption), true); } else { self.selectUnknownOrEmptyOption(value); } }; // Tell the select control that an option, with the given value, has been added self.addOption = function(value, element) { // Skip comment nodes, as they only pollute the `optionsMap` if (element[0].nodeType === NODE_TYPE_COMMENT) return; assertNotHasOwnProperty(value, '"option value"'); if (value === '') { self.hasEmptyOption = true; self.emptyOption = element; } var count = optionsMap.get(value) || 0; optionsMap.set(value, count + 1); // Only render at the end of a digest. This improves render performance when many options // are added during a digest and ensures all relevant options are correctly marked as selected scheduleRender(); }; // 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.delete(value); if (value === '') { self.hasEmptyOption = false; self.emptyOption = undefined; } } else { optionsMap.set(value, count - 1); } } }; // Check whether the select control has an option matching the given value self.hasOption = function(value) { return !!optionsMap.get(value); }; /** * @ngdoc method * @name select.SelectController#$hasEmptyOption * * @description * * Returns `true` if the select element currently has an empty option * element, i.e. an option that signifies that the select is empty / the selection is null. * */ self.$hasEmptyOption = function() { return self.hasEmptyOption; }; /** * @ngdoc method * @name select.SelectController#$isUnknownOptionSelected * * @description * * Returns `true` if the select element's unknown option is selected. The unknown option is added * and automatically selected whenever the select model doesn't match any option. * */ self.$isUnknownOptionSelected = function() { // Presence of the unknown option means it is selected return $element[0].options[0] === self.unknownOption[0]; }; /** * @ngdoc method * @name select.SelectController#$isEmptyOptionSelected * * @description * * Returns `true` if the select element has an empty option and this empty option is currently * selected. Returns `false` if the select element has no empty option or it is not selected. * */ self.$isEmptyOptionSelected = function() { return self.hasEmptyOption && $element[0].options[$element[0].selectedIndex] === self.emptyOption[0]; }; self.selectUnknownOrEmptyOption = function(value) { if (value == null && self.emptyOption) { self.removeUnknownOption(); self.selectEmptyOption(); } else if (self.unknownOption.parent().length) { self.updateUnknownOption(value); } else { self.renderUnknownOption(value); } }; var renderScheduled = false; function scheduleRender() { if (renderScheduled) return; renderScheduled = true; $scope.$$postDigest(function() { renderScheduled = false; self.ngModelCtrl.$render(); }); } var updateScheduled = false; function scheduleViewValueUpdate(renderAfter) { if (updateScheduled) return; updateScheduled = true; $scope.$$postDigest(function() { if ($scope.$$destroyed) return; updateScheduled = false; self.ngModelCtrl.$setViewValue(self.readValue()); if (renderAfter) self.ngModelCtrl.$render(); }); } self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) { if (optionAttrs.$attr.ngValue) { // The value attribute is set by ngValue var oldVal, hashedVal; optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) { var removal; var previouslySelected = optionElement.prop('selected'); if (isDefined(hashedVal)) { self.removeOption(oldVal); delete self.selectValueMap[hashedVal]; removal = true; } hashedVal = hashKey(newVal); oldVal = newVal; self.selectValueMap[hashedVal] = newVal; self.addOption(newVal, optionElement); // Set the attribute directly instead of using optionAttrs.$set - this stops the observer // from firing a second time. Other $observers on value will also get the result of the // ngValue expression, not the hashed value optionElement.attr('value', hashedVal); if (removal && previouslySelected) { scheduleViewValueUpdate(); } }); } else if (interpolateValueFn) { // The value attribute is interpolated optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) { // This method is overwritten in ngOptions and has side-effects! self.readValue(); var removal; var previouslySelected = optionElement.prop('selected'); if (isDefined(oldVal)) { self.removeOption(oldVal); removal = true; } oldVal = newVal; self.addOption(newVal, optionElement); if (removal && previouslySelected) { scheduleViewValueUpdate(); } }); } else if (interpolateTextFn) { // The text content is interpolated optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) { optionAttrs.$set('value', newVal); var previouslySelected = optionElement.prop('selected'); if (oldVal !== newVal) { self.removeOption(oldVal); } self.addOption(newVal, optionElement); if (oldVal && previouslySelected) { scheduleViewValueUpdate(); } }); } else { // The value attribute is static self.addOption(optionAttrs.value, optionElement); } optionAttrs.$observe('disabled', function(newVal) { // Since model updates will also select disabled options (like ngOptions), // we only have to handle options becoming disabled, not enabled if (newVal === 'true' || newVal && optionElement.prop('selected')) { if (self.multiple) { scheduleViewValueUpdate(true); } else { self.ngModelCtrl.$setViewValue(null); self.ngModelCtrl.$render(); } } }); optionElement.on('$destroy', function() { var currentValue = self.readValue(); var removeValue = optionAttrs.value; self.removeOption(removeValue); scheduleRender(); if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 || currentValue === removeValue ) { // When multiple (selected) options are destroyed at the same time, we don't want // to run a model update for each of them. Instead, run a single update in the $$postDigest scheduleViewValueUpdate(true); } }); }; }]; /** * @ngdoc directive * @name select * @restrict E * * @description * HTML `select` element with AngularJS data-binding. * * The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding * between the scope and the `<select>` control (including setting default values). * It also handles dynamic `<option>` elements, which can be added using the {@link ngRepeat `ngRepeat}` or * {@link ngOptions `ngOptions`} directives. * * When an item in the `<select>` menu is selected, the value of the selected option will be bound * to the model identified by the `ngModel` directive. With static or repeated options, this is * the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing. * Value and textContent can be interpolated. * * The {@link select.SelectController select controller} exposes utility functions that can be used * to manipulate the select's behavior. * * ## Matching model and option values * * In general, the match between the model and an option is evaluated by strictly comparing the model * value against the value of the available options. * * If you are setting the option value with the option's `value` attribute, or textContent, the * value will always be a `string` which means that the model value must also be a string. * Otherwise the `select` directive cannot match them correctly. * * To bind the model to a non-string value, you can use one of the following strategies: * - the {@link ng.ngOptions `ngOptions`} directive * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value}) * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example}) * - model $parsers / $formatters to convert the string value * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example}) * * If the viewValue of `ngModel` does not match any of the options, then the control * will automatically add an "unknown" option, which it then removes when the mismatch 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. * * ## Choosing between `ngRepeat` and `ngOptions` * * In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits: * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the * comprehension expression * - reduced memory consumption by not creating a new scope for each repeated instance * - increased render speed by creating the options in a documentFragment instead of individually * * Specifically, select with repeated options slows down significantly starting at 2000 options in * Chrome and Internet Explorer / Edge. * * * @param {string} ngModel Assignable AngularJS expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} multiple Allows multiple options to be selected. The selected values will be * bound to the model as an array. * @param {string=} required Sets `required` validation error key if the value is not 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 {string=} ngChange AngularJS expression to be executed when selected option(s) changes due to user * interaction with the select element. * @param {string=} ngOptions sets the options that the select is populated with and defines what is * set on the model on selection. See {@link ngOptions `ngOptions`}. * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive. * * * @example * ### Simple `select` elements with static options * * <example name="static-select" module="staticSelect"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="myForm"> * <label for="singleSelect"> Single select: </label><br> * <select name="singleSelect" ng-model="data.singleSelect"> * <option value="option-1">Option 1</option> * <option value="option-2">Option 2</option> * </select><br> * * <label for="singleSelect"> Single select with "not selected" option and dynamic option values: </label><br> * <select name="singleSelect" id="singleSelect" ng-model="data.singleSelect"> * <option value="">---Please select---</option> <!-- not selected / blank option --> * <option value="{{data.option1}}">Option 1</option> <!-- interpolation --> * <option value="option-2">Option 2</option> * </select><br> * <button ng-click="forceUnknownOption()">Force unknown option</button><br> * <tt>singleSelect = {{data.singleSelect}}</tt> * * <hr> * <label for="multipleSelect"> Multiple select: </label><br> * <select name="multipleSelect" id="multipleSelect" ng-model="data.multipleSelect" multiple> * <option value="option-1">Option 1</option> * <option value="option-2">Option 2</option> * <option value="option-3">Option 3</option> * </select><br> * <tt>multipleSelect = {{data.multipleSelect}}</tt><br/> * </form> * </div> * </file> * <file name="app.js"> * angular.module('staticSelect', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.data = { * singleSelect: null, * multipleSelect: [], * option1: 'option-1' * }; * * $scope.forceUnknownOption = function() { * $scope.data.singleSelect = 'nonsense'; * }; * }]); * </file> *</example> * * @example * ### Using `ngRepeat` to generate `select` options * <example name="select-ngrepeat" module="ngrepeatSelect"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="myForm"> * <label for="repeatSelect"> Repeat select: </label> * <select name="repeatSelect" id="repeatSelect" ng-model="data.model"> * <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option> * </select> * </form> * <hr> * <tt>model = {{data.model}}</tt><br/> * </div> * </file> * <file name="app.js"> * angular.module('ngrepeatSelect', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.data = { * model: null, * availableOptions: [ * {id: '1', name: 'Option A'}, * {id: '2', name: 'Option B'}, * {id: '3', name: 'Option C'} * ] * }; * }]); * </file> *</example> * * @example * ### Using `ngValue` to bind the model to an array of objects * <example name="select-ngvalue" module="ngvalueSelect"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="myForm"> * <label for="ngvalueselect"> ngvalue select: </label> * <select size="6" name="ngvalueselect" ng-model="data.model" multiple> * <option ng-repeat="option in data.availableOptions" ng-value="option.value">{{option.name}}</option> * </select> * </form> * <hr> * <pre>model = {{data.model | json}}</pre><br/> * </div> * </file> * <file name="app.js"> * angular.module('ngvalueSelect', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.data = { * model: null, * availableOptions: [ {value: 'myString', name: 'string'}, {value: 1, name: 'integer'}, {value: true, name: 'boolean'}, {value: null, name: 'null'}, {value: {prop: 'value'}, name: 'object'}, {value: ['a'], name: 'array'} * ] * }; * }]); * </file> *</example> * * @example * ### Using `select` with `ngOptions` and setting a default value * See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples. * * <example name="select-with-default-values" module="defaultValueSelect"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="myForm"> * <label for="mySelect">Make a choice:</label> * <select name="mySelect" id="mySelect" * ng-options="option.name for option in data.availableOptions track by option.id" * ng-model="data.selectedOption"></select> * </form> * <hr> * <tt>option = {{data.selectedOption}}</tt><br/> * </div> * </file> * <file name="app.js"> * angular.module('defaultValueSelect', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.data = { * availableOptions: [ * {id: '1', name: 'Option A'}, * {id: '2', name: 'Option B'}, * {id: '3', name: 'Option C'} * ], * selectedOption: {id: '3', name: 'Option C'} //This sets the default value of the select in the ui * }; * }]); * </file> *</example> * * @example * ### Binding `select` to a non-string value via `ngModel` parsing / formatting * * <example name="select-with-non-string-options" module="nonStringSelect"> * <file name="index.html"> * <select ng-model="model.id" convert-to-number> * <option value="0">Zero</option> * <option value="1">One</option> * <option value="2">Two</option> * </select> * {{ model }} * </file> * <file name="app.js"> * angular.module('nonStringSelect', []) * .run(function($rootScope) { * $rootScope.model = { id: 2 }; * }) * .directive('convertToNumber', function() { * return { * require: 'ngModel', * link: function(scope, element, attrs, ngModel) { * ngModel.$parsers.push(function(val) { * return parseInt(val, 10); * }); * ngModel.$formatters.push(function(val) { * return '' + val; * }); * } * }; * }); * </file> * <file name="protractor.js" type="protractor"> * it('should initialize to model', function() { * expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two'); * }); * </file> * </example> * */ var selectDirective = function() { return { restrict: 'E', require: ['select', '?ngModel'], controller: SelectController, priority: 1, link: { pre: selectPreLink, post: selectPostLink } }; function selectPreLink(scope, element, attr, ctrls) { var selectCtrl = ctrls[0]; var ngModelCtrl = ctrls[1]; // if ngModel is not defined, we don't need to do anything but set the registerOption // function to noop, so options don't get added internally if (!ngModelCtrl) { selectCtrl.registerOption = noop; return; } selectCtrl.ngModelCtrl = ngModelCtrl; // 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() { selectCtrl.removeUnknownOption(); scope.$apply(function() { ngModelCtrl.$setViewValue(selectCtrl.readValue()); }); }); // 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) { selectCtrl.multiple = true; // 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 && !option.disabled) { var val = option.value; array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val); } }); return array; }; // Write value now needs to set the selected property of each matching option selectCtrl.writeValue = function writeMultipleValue(value) { forEach(element.find('option'), function(option) { var shouldBeSelected = !!value && (includes(value, option.value) || includes(value, selectCtrl.selectValueMap[option.value])); var currentlySelected = option.selected; // Support: IE 9-11 only, Edge 12-15+ // In IE and Edge adding options to the selection via shift+click/UP/DOWN // will de-select already selected options if "selected" on those options was set // more than once (i.e. when the options were already selected) // So we only modify the selected property if necessary. // Note: this behavior cannot be replicated via unit tests because it only shows in the // actual user interface. if (shouldBeSelected !== currentlySelected) { setOptionSelectedStatus(jqLite(option), shouldBeSelected); } }); }; // 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 var lastView, lastViewRef = NaN; scope.$watch(function selectMultipleWatch() { if (lastViewRef === ngModelCtrl.$viewValue && !equals(lastView, ngModelCtrl.$viewValue)) { lastView = shallowCopy(ngModelCtrl.$viewValue); ngModelCtrl.$render(); } lastViewRef = ngModelCtrl.$viewValue; }); // 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; }; } } function selectPostLink(scope, element, attrs, ctrls) { // if ngModel is not defined, we don't need to do anything var ngModelCtrl = ctrls[1]; if (!ngModelCtrl) return; var selectCtrl = ctrls[0]; // 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`. // This must be done in the postLink fn to prevent $render to be called before // all nodes have been linked correctly. ngModelCtrl.$render = function() { selectCtrl.writeValue(ngModelCtrl.$viewValue); }; } }; // 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) { return { restrict: 'E', priority: 100, compile: function(element, attr) { var interpolateValueFn, interpolateTextFn; if (isDefined(attr.ngValue)) { // Will be handled by registerOption } else if (isDefined(attr.value)) { // If the value attribute is defined, check if it contains an interpolation interpolateValueFn = $interpolate(attr.value, true); } else { // If the value attribute is not defined then we fall back to the // text content of the option element, which may be interpolated interpolateTextFn = $interpolate(element.text(), true); if (!interpolateTextFn) { attr.$set('value', element.text()); } } 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.registerOption(scope, element, attr, interpolateValueFn, interpolateTextFn); } }; } }; }];