feat(select): expose info about selection state in controller
This allows custom directives to manipulate the select's and ngModel's behavior based on the state of the unknown and the empty option. Closes #13172 Closes #10127
This commit is contained in:
committed by
Martin Staffa
parent
2fdfbe7296
commit
6fc0f02ad2
+159
-3
@@ -19,10 +19,120 @@ function setOptionSelectedStatus(optionEl, value) {
|
||||
/**
|
||||
* @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.
|
||||
* 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 ng-if="myForm.testSelect.$error.unknownValue">Error: The current model doesn't match any option</span>
|
||||
*
|
||||
* <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" unknown-value-required>
|
||||
* <option value="option-1">Option 1</option>
|
||||
* <option value="option-2">Option 2</option>
|
||||
* </select><br>
|
||||
* <span 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>
|
||||
*</example>
|
||||
*
|
||||
*
|
||||
*/
|
||||
var SelectController =
|
||||
['$element', '$scope', /** @this */ function($element, $scope) {
|
||||
@@ -172,6 +282,49 @@ var SelectController =
|
||||
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();
|
||||
@@ -330,6 +483,9 @@ var SelectController =
|
||||
* 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
|
||||
|
||||
@@ -3305,4 +3305,94 @@ describe('ngOptions', function() {
|
||||
expect(scope.form.select.$pristine).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectCtrl api', function() {
|
||||
|
||||
it('should reflect the status of empty and unknown option', function() {
|
||||
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
|
||||
|
||||
var selectCtrl = element.controller('select');
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.values = [{name: 'A'}, {name: 'B'}];
|
||||
scope.isBlank = true;
|
||||
});
|
||||
|
||||
expect(element).toEqualSelect([''], 'object:4', 'object:5');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// empty -> selection
|
||||
scope.$apply(function() {
|
||||
scope.selected = scope.values[0];
|
||||
});
|
||||
|
||||
expect(element).toEqualSelect('', ['object:4'], 'object:5');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// remove empty
|
||||
scope.$apply('isBlank = false');
|
||||
|
||||
expect(element).toEqualSelect(['object:4'], 'object:5');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(false);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// selection -> unknown
|
||||
scope.$apply('selected = "unmatched"');
|
||||
|
||||
expect(element).toEqualSelect(['?'], 'object:4', 'object:5');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(false);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
||||
|
||||
// add empty
|
||||
scope.$apply('isBlank = true');
|
||||
|
||||
expect(element).toEqualSelect(['?'], '', 'object:4', 'object:5');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
||||
|
||||
// unknown -> empty
|
||||
scope.$apply(function() {
|
||||
scope.selected = null;
|
||||
});
|
||||
|
||||
expect(element).toEqualSelect([''], 'object:4', 'object:5');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// empty -> unknown
|
||||
scope.$apply('selected = "unmatched"');
|
||||
|
||||
expect(element).toEqualSelect(['?'], '', 'object:4', 'object:5');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
||||
|
||||
// unknown -> selection
|
||||
scope.$apply(function() {
|
||||
scope.selected = scope.values[1];
|
||||
});
|
||||
|
||||
expect(element).toEqualSelect('', 'object:4', ['object:5']);
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// selection -> empty
|
||||
scope.$apply('selected = null');
|
||||
|
||||
expect(element).toEqualSelect([''], 'object:4', 'object:5');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -434,34 +434,30 @@ describe('select', function() {
|
||||
describe('empty option', function() {
|
||||
|
||||
it('should allow empty option to be added and removed dynamically', function() {
|
||||
|
||||
scope.dynamicOptions = [];
|
||||
scope.robot = '';
|
||||
compile('<select ng-model="robot">' +
|
||||
'<option ng-repeat="opt in dynamicOptions" value="{{opt.val}}">{{opt.display}}</option>' +
|
||||
'</select>');
|
||||
|
||||
expect(element).toEqualSelect(['? string: ?']);
|
||||
|
||||
|
||||
scope.dynamicOptions = [
|
||||
{ val: '', display: '--select--' },
|
||||
{ val: '', display: '--empty--'},
|
||||
{ val: 'x', display: 'robot x' },
|
||||
{ val: 'y', display: 'robot y' }
|
||||
];
|
||||
scope.$digest();
|
||||
expect(element).toEqualSelect([''], 'x', 'y');
|
||||
|
||||
|
||||
scope.robot = 'x';
|
||||
scope.$digest();
|
||||
expect(element).toEqualSelect('', ['x'], 'y');
|
||||
|
||||
|
||||
scope.dynamicOptions.shift();
|
||||
scope.$digest();
|
||||
expect(element).toEqualSelect(['x'], 'y');
|
||||
|
||||
|
||||
scope.robot = undefined;
|
||||
scope.$digest();
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'x', 'y');
|
||||
@@ -839,6 +835,109 @@ describe('select', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectController', function() {
|
||||
|
||||
it('should expose .$hasEmptyOption(), .$isEmptyOptionSelected(), ' +
|
||||
'and .$isUnknownOptionSelected()', function() {
|
||||
compile('<select ng-model="mySelect"></select>');
|
||||
|
||||
var selectCtrl = element.controller('select');
|
||||
|
||||
expect(selectCtrl.$hasEmptyOption).toEqual(jasmine.any(Function));
|
||||
expect(selectCtrl.$isEmptyOptionSelected).toEqual(jasmine.any(Function));
|
||||
expect(selectCtrl.$isUnknownOptionSelected).toEqual(jasmine.any(Function));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should reflect the status of empty and unknown option', function() {
|
||||
scope.dynamicOptions = [];
|
||||
scope.selected = '';
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-if="empty" value="">--no selection--</option>' +
|
||||
'<option ng-repeat="opt in dynamicOptions" value="{{opt.val}}">{{opt.display}}</option>' +
|
||||
'</select>');
|
||||
|
||||
var selectCtrl = element.controller('select');
|
||||
|
||||
expect(element).toEqualSelect(['? string: ?']);
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(false);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
|
||||
scope.dynamicOptions = [
|
||||
{ val: 'x', display: 'robot x' },
|
||||
{ val: 'y', display: 'robot y' }
|
||||
];
|
||||
scope.empty = true;
|
||||
|
||||
scope.$digest();
|
||||
expect(element).toEqualSelect([''], 'x', 'y');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// empty -> selection
|
||||
scope.$apply('selected = "x"');
|
||||
expect(element).toEqualSelect('', ['x'], 'y');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// remove empty
|
||||
scope.$apply('empty = false');
|
||||
expect(element).toEqualSelect(['x'], 'y');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(false);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// selection -> unknown
|
||||
scope.$apply('selected = "unmatched"');
|
||||
expect(element).toEqualSelect([unknownValue('unmatched')], 'x', 'y');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(false);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
||||
|
||||
// add empty
|
||||
scope.$apply('empty = true');
|
||||
expect(element).toEqualSelect([unknownValue('unmatched')], '', 'x', 'y');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
||||
|
||||
// unknown -> empty
|
||||
scope.$apply('selected = null');
|
||||
|
||||
expect(element).toEqualSelect([''], 'x', 'y');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// empty -> unknown
|
||||
scope.$apply('selected = "unmatched"');
|
||||
|
||||
expect(element).toEqualSelect([unknownValue('unmatched')], '', 'x', 'y');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
||||
|
||||
// unknown -> selection
|
||||
scope.$apply('selected = "y"');
|
||||
|
||||
expect(element).toEqualSelect('', 'x', ['y']);
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
|
||||
// selection -> empty
|
||||
scope.$apply('selected = null');
|
||||
|
||||
expect(element).toEqualSelect([''], 'x', 'y');
|
||||
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
||||
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
||||
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('selectController.hasOption', function() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user