Skip to content

Commit 121773d

Browse files
committed
feat(select): expose info about selection state
Closes angular#13172 Closes angular#10127
1 parent 0b874c0 commit 121773d

File tree

4 files changed

+357
-10
lines changed

4 files changed

+357
-10
lines changed

src/ng/directive/ngOptions.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
569569
ngModelCtrl.$render();
570570

571571
optionEl.on('$destroy', function() {
572-
var needsRerender = selectCtrl.isEmptyOptionSelected();
572+
var needsRerender = selectCtrl.$isEmptyOptionSelected();
573573

574574
selectCtrl.hasEmptyOption = false;
575575
selectCtrl.emptyOption = undefined;

src/ng/directive/select.js

+157-3
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,120 @@ function setOptionSelectedStatus(optionEl, value) {
1919
/**
2020
* @ngdoc type
2121
* @name select.SelectController
22+
*
2223
* @description
23-
* The controller for the `<select>` directive. This provides support for reading
24-
* and writing the selected value(s) of the control and also coordinates dynamically
25-
* added `<option>` elements, perhaps by an `ngRepeat` directive.
24+
* The controller for the {@link ng.select select directive}. The controller exposes
25+
* a few utility methods that can be used to augment the behavior of a regular or an
26+
* {@link ng.ngOptions ngOptions} select element.
27+
*
28+
* @example
29+
* ### Set a custom error when the unknown option is selected
30+
*
31+
* This example sets a custom error "unknown_value" on the ngModelController
32+
* when the select element's unknown option is selected, i.e. when the model is set to value
33+
* that is not matched by any option.
34+
*
35+
* <example name="select-unknown-value-error" module="staticSelect">
36+
* <file name="index.html">
37+
* <div ng-controller="ExampleController">
38+
* <form name="myForm">
39+
* <label for="testSelect"> Single select: </label><br>
40+
* <select name="testSelect" ng-model="selected" unknown-value-error>
41+
* <option value="option-1">Option 1</option>
42+
* <option value="option-2">Option 2</option>
43+
* </select><br>
44+
* <span ng-if="myForm.testSelect.$error.unknown_value">Error: The current model doesn't match any option</span>
45+
*
46+
* <button ng-click="forceUnknownOption()">Force unknown option</button><br>
47+
* </form>
48+
* </div>
49+
* </file>
50+
* <file name="app.js">
51+
* angular.module('staticSelect', [])
52+
* .controller('ExampleController', ['$scope', function($scope) {
53+
* $scope.selected = null;
54+
*
55+
* $scope.forceUnknownOption = function() {
56+
* $scope.selected = 'nonsense';
57+
* };
58+
* }])
59+
* .directive('unknownValueError', function() {
60+
* return {
61+
* require: ['ngModel', 'select'],
62+
* link: function(scope, element, attrs, ctrls) {
63+
* var ngModelCtrl = ctrls[0];
64+
* var selectCtrl = ctrls[1];
65+
*
66+
* ngModelCtrl.$validators.unknown_value = function(modelValue, viewValue) {
67+
* if (selectCtrl.$isUnknownOptionSelected()) {
68+
* return false;
69+
* }
70+
*
71+
* return true;
72+
* };
73+
* }
74+
*
75+
* };
76+
* });
77+
* </file>
78+
*</example>
79+
*
80+
*
81+
* @example
82+
* ### Set the "required" error when the unknown option is selected.
83+
*
84+
* By default, the "required" error on the ngModelController is only set on a required select
85+
* when the empty option is selected. This example adds a custom directive that also sets the
86+
* error when the unknown option is selected.
87+
*
88+
* <example name="select-unknown-value-required" module="staticSelect">
89+
* <file name="index.html">
90+
* <div ng-controller="ExampleController">
91+
* <form name="myForm">
92+
* <label for="testSelect"> Select: </label><br>
93+
* <select name="testSelect" ng-model="selected" unknown-value-required>
94+
* <option value="option-1">Option 1</option>
95+
* <option value="option-2">Option 2</option>
96+
* </select><br>
97+
* <span ng-if="myForm.testSelect.$error.required">Error: Please select a value</span><br>
98+
*
99+
* <button ng-click="forceUnknownOption()">Force unknown option</button><br>
100+
* </form>
101+
* </div>
102+
* </file>
103+
* <file name="app.js">
104+
* angular.module('staticSelect', [])
105+
* .controller('ExampleController', ['$scope', function($scope) {
106+
* $scope.selected = null;
107+
*
108+
* $scope.forceUnknownOption = function() {
109+
* $scope.selected = 'nonsense';
110+
* };
111+
* }])
112+
* .directive('unknownValueRequired', function() {
113+
* return {
114+
* priority: 1, // This directive must run after the required directive has added its validator
115+
* require: ['ngModel', 'select'],
116+
* link: function(scope, element, attrs, ctrls) {
117+
* var ngModelCtrl = ctrls[0];
118+
* var selectCtrl = ctrls[1];
119+
*
120+
* var originalRequiredValidator = ngModelCtrl.$validators.required;
121+
*
122+
* ngModelCtrl.$validators.required = function() {
123+
* if (attrs.required && selectCtrl.$isUnknownOptionSelected()) {
124+
* return false;
125+
* }
126+
*
127+
* return originalRequiredValidator.apply(this, arguments);
128+
* };
129+
* }
130+
* };
131+
* });
132+
* </file>
133+
*</example>
134+
*
135+
*
26136
*/
27137
var SelectController =
28138
['$element', '$scope', /** @this */ function($element, $scope) {
@@ -171,6 +281,50 @@ var SelectController =
171281
return !!optionsMap.get(value);
172282
};
173283

284+
/**
285+
* @ngdoc method
286+
* @name select.SelectController#$hasEmptyOption
287+
*
288+
* @description
289+
*
290+
* Returns `true` if the select element currently has an empty option
291+
* element, i.e. an option that signifies that the select is empty / the selection is null.
292+
*
293+
*/
294+
self.$hasEmptyOption = function() {
295+
// Presence of the unknown option means it is selected
296+
return self.hasEmptyOption;
297+
};
298+
299+
/**
300+
* @ngdoc method
301+
* @name select.SelectController#$isUnknownOptionSelected
302+
*
303+
* @description
304+
*
305+
* Returns `true` if the select element's unknown option is selected. The unknown option is added
306+
* and automatically selected whenever the select model doesn't match any option.
307+
*
308+
*/
309+
self.$isUnknownOptionSelected = function() {
310+
// Presence of the unknown option means it is selected
311+
return $element[0].options[0] === self.unknownOption[0];
312+
};
313+
314+
/**
315+
* @ngdoc method
316+
* @name select.SelectController#$isEmptyOptionSelected
317+
*
318+
* @description
319+
*
320+
* Returns `true` if the select element has an empty option and this empty option is currently
321+
* selected. Returns `false` if the select element has no empty option or it is not selected.
322+
*
323+
*/
324+
self.$isEmptyOptionSelected = function() {
325+
return self.hasEmptyOption && $element[0].options[$element[0].selectedIndex] === self.emptyOption[0];
326+
};
327+
174328
self.selectUnknownOrEmptyOption = function(value) {
175329
if (value == null && self.emptyOption) {
176330
self.removeUnknownOption();

test/ng/directive/ngOptionsSpec.js

+90
Original file line numberDiff line numberDiff line change
@@ -3296,4 +3296,94 @@ describe('ngOptions', function() {
32963296
expect(scope.form.select.$pristine).toBe(true);
32973297
});
32983298
});
3299+
3300+
describe('selectCtrl api', function() {
3301+
3302+
it('should reflect the status of empty and unknown option', function() {
3303+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
3304+
3305+
var selectCtrl = element.controller('select');
3306+
3307+
scope.$apply(function() {
3308+
scope.values = [{name: 'A'}, {name: 'B'}];
3309+
scope.isBlank = true;
3310+
});
3311+
3312+
expect(element).toEqualSelect([''], 'object:4', 'object:5');
3313+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
3314+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
3315+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3316+
3317+
// empty -> selection
3318+
scope.$apply(function() {
3319+
scope.selected = scope.values[0];
3320+
});
3321+
3322+
expect(element).toEqualSelect('', ['object:4'], 'object:5');
3323+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
3324+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3325+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3326+
3327+
// remove empty
3328+
scope.$apply('isBlank = false');
3329+
3330+
expect(element).toEqualSelect(['object:4'], 'object:5');
3331+
expect(selectCtrl.$hasEmptyOption()).toBe(false);
3332+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3333+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3334+
3335+
// selection -> unknown
3336+
scope.$apply('selected = "unmatched"');
3337+
3338+
expect(element).toEqualSelect(['?'], 'object:4', 'object:5');
3339+
expect(selectCtrl.$hasEmptyOption()).toBe(false);
3340+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3341+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
3342+
3343+
// add empty
3344+
scope.$apply('isBlank = true');
3345+
3346+
expect(element).toEqualSelect(['?'], '', 'object:4', 'object:5');
3347+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
3348+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3349+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
3350+
3351+
// unknown -> empty
3352+
scope.$apply(function() {
3353+
scope.selected = null;
3354+
});
3355+
3356+
expect(element).toEqualSelect([''], 'object:4', 'object:5');
3357+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
3358+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
3359+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3360+
3361+
// empty -> unknown
3362+
scope.$apply('selected = "unmatched"');
3363+
3364+
expect(element).toEqualSelect(['?'], '', 'object:4', 'object:5');
3365+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
3366+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3367+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
3368+
3369+
// unknown -> selection
3370+
scope.$apply(function() {
3371+
scope.selected = scope.values[1];
3372+
});
3373+
3374+
expect(element).toEqualSelect('', 'object:4', ['object:5']);
3375+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
3376+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3377+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3378+
3379+
// selection -> empty
3380+
scope.$apply('selected = null');
3381+
3382+
expect(element).toEqualSelect([''], 'object:4', 'object:5');
3383+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
3384+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
3385+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3386+
});
3387+
});
3388+
32993389
});

0 commit comments

Comments
 (0)