Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit e0c1ad7

Browse files
committed
feat(ngOptions): add support for disabling an option
This patch adds support for disabling options based on model values. The "disable when" syntax allows for listening to changes on those model values, in order to dynamically enable and disable the options. The changes prevent disabled options from being written to the selectCtrl from the model. If a disabled selection is present on the model, normal unknown or empty functionality kicks in. Additionally, instead of adding more unnecessary watchers, if either label or disableWhen are not matched in the ngOptions expression, they will not be watched. closes #638 closes #11050
1 parent 3c6a0e5 commit e0c1ad7

File tree

2 files changed

+158
-25
lines changed

2 files changed

+158
-25
lines changed

src/ng/directive/ngOptions.js

+51-25
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,20 @@ var ngOptionsMinErr = minErr('ngOptions');
9494
* * `label` **`for`** `value` **`in`** `array`
9595
* * `select` **`as`** `label` **`for`** `value` **`in`** `array`
9696
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
97+
* * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array`
9798
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
99+
* * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
98100
* * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
99101
* (for including a filter with `track by`)
100102
* * for object data sources:
101103
* * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
102104
* * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
103105
* * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
106+
* * `label` **`disable when`** `disable` **`for (`**`key`**`,`** `value`**`) in`** `object`
104107
* * `select` **`as`** `label` **`group by`** `group`
105108
* **`for` `(`**`key`**`,`** `value`**`) in`** `object`
109+
* * `select` **`as`** `label` **`disable when`** `disable`
110+
* **`for` `(`**`key`**`,`** `value`**`) in`** `object`
106111
*
107112
* Where:
108113
*
@@ -116,6 +121,8 @@ var ngOptionsMinErr = minErr('ngOptions');
116121
* element. If not specified, `select` expression will default to `value`.
117122
* * `group`: The result of this expression will be used to group options using the `<optgroup>`
118123
* DOM element.
124+
* * `disable`: The result of this expression will be used to disable the rendered `<option>`
125+
* element. Return `true` to disable.
119126
* * `trackexpr`: Used when working with an array of objects. The result of this expression will be
120127
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
121128
* `value` variable (e.g. `value.propertyName`). With this the selection is preserved
@@ -129,10 +136,10 @@ var ngOptionsMinErr = minErr('ngOptions');
129136
.controller('ExampleController', ['$scope', function($scope) {
130137
$scope.colors = [
131138
{name:'black', shade:'dark'},
132-
{name:'white', shade:'light'},
139+
{name:'white', shade:'light', notAnOption: true},
133140
{name:'red', shade:'dark'},
134-
{name:'blue', shade:'dark'},
135-
{name:'yellow', shade:'light'}
141+
{name:'blue', shade:'dark', notAnOption: true},
142+
{name:'yellow', shade:'light', notAnOption: false}
136143
];
137144
$scope.myColor = $scope.colors[2]; // red
138145
}]);
@@ -141,6 +148,7 @@ var ngOptionsMinErr = minErr('ngOptions');
141148
<ul>
142149
<li ng-repeat="color in colors">
143150
Name: <input ng-model="color.name">
151+
<input type="checkbox" ng-model="color.notAnOption"> Disabled?
144152
[<a href ng-click="colors.splice($index, 1)">X</a>]
145153
</li>
146154
<li>
@@ -162,6 +170,12 @@ var ngOptionsMinErr = minErr('ngOptions');
162170
<select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
163171
</select><br/>
164172
173+
Color grouped by shade, with some disabled:
174+
<select ng-model="myColor"
175+
ng-options="color.name group by color.shade disable when color.notAnOption for color in colors">
176+
</select><br/>
177+
178+
165179
166180
Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br>
167181
<hr/>
@@ -186,16 +200,17 @@ var ngOptionsMinErr = minErr('ngOptions');
186200
*/
187201

188202
// jshint maxlen: false
189-
//000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
190-
var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
203+
// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999
204+
var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
191205
// 1: value expression (valueFn)
192206
// 2: label expression (displayFn)
193207
// 3: group by expression (groupByFn)
194-
// 4: array item variable name
195-
// 5: object item key variable name
196-
// 6: object item value variable name
197-
// 7: collection expression
198-
// 8: track by expression
208+
// 4: disable when expression (disableWhenFn)
209+
// 5: array item variable name
210+
// 6: object item key variable name
211+
// 7: object item value variable name
212+
// 8: collection expression
213+
// 9: track by expression
199214
// jshint maxlen: 100
200215

201216

@@ -215,14 +230,14 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
215230
// Extract the parts from the ngOptions expression
216231

217232
// The variable name for the value of the item in the collection
218-
var valueName = match[4] || match[6];
233+
var valueName = match[5] || match[7];
219234
// The variable name for the key of the item in the collection
220-
var keyName = match[5];
235+
var keyName = match[6];
221236

222237
// An expression that generates the viewValue for an option if there is a label expression
223238
var selectAs = / as /.test(match[0]) && match[1];
224239
// An expression that is used to track the id of each object in the options collection
225-
var trackBy = match[8];
240+
var trackBy = match[9];
226241
// An expression that generates the viewValue for an option if there is no label expression
227242
var valueFn = $parse(match[2] ? match[1] : valueName);
228243
var selectAsFn = selectAs && $parse(selectAs);
@@ -237,7 +252,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
237252
function getHashOfValue(viewValue) { return hashKey(viewValue); };
238253
var displayFn = $parse(match[2] || match[1]);
239254
var groupByFn = $parse(match[3] || '');
240-
var valuesFn = $parse(match[7]);
255+
var disableWhenFn = $parse(match[4] || '');
256+
var valuesFn = $parse(match[8]);
241257

242258
var locals = {};
243259
var getLocals = keyName ? function(value, key) {
@@ -250,11 +266,12 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
250266
};
251267

252268

253-
function Option(selectValue, viewValue, label, group) {
269+
function Option(selectValue, viewValue, label, group, disabled) {
254270
this.selectValue = selectValue;
255271
this.viewValue = viewValue;
256272
this.label = label;
257273
this.group = group;
274+
this.disabled = disabled;
258275
}
259276

260277
return {
@@ -269,8 +286,11 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
269286
var locals = getLocals(values[key], key);
270287
var label = displayFn(scope, locals);
271288
var selectValue = getTrackByValue(values[key], locals);
289+
var disableWhen = disableWhenFn(scope, locals);
272290
watchedArray.push(selectValue);
273-
watchedArray.push(label);
291+
// for optimization, do not watch label or disableWhen if they weren't defined
292+
if (match[2]) watchedArray.push(label);
293+
if (match[4]) watchedArray.push(disableWhen);
274294
});
275295
return watchedArray;
276296
}),
@@ -296,7 +316,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
296316
var selectValue = getTrackByValue(viewValue, locals);
297317
var label = displayFn(scope, locals);
298318
var group = groupByFn(scope, locals);
299-
var optionItem = new Option(selectValue, viewValue, label, group);
319+
var disabled = disableWhenFn(scope, locals);
320+
var optionItem = new Option(selectValue, viewValue, label, group, disabled);
300321

301322
optionItems.push(optionItem);
302323
selectValueMap[selectValue] = optionItem;
@@ -322,7 +343,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
322343
return {
323344
restrict: 'A',
324345
terminal: true,
325-
require: ['select', '?ngModel'],
346+
require: ['select', 'ngModel'],
326347
link: function(scope, selectElement, attr, ctrls) {
327348

328349
// if ngModel is not defined, we don't need to do anything
@@ -373,7 +394,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
373394
selectCtrl.writeValue = function writeNgOptionsValue(value) {
374395
var option = options.getOptionFromViewValue(value);
375396

376-
if (option) {
397+
if (option && !option.disabled) {
377398
if (selectElement[0].value !== option.selectValue) {
378399
removeUnknownOption();
379400
removeEmptyOption();
@@ -397,7 +418,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
397418

398419
var selectedOption = options.selectValueMap[selectElement.val()];
399420

400-
if (selectedOption) {
421+
if (selectedOption && !selectedOption.disabled) {
401422
removeEmptyOption();
402423
removeUnknownOption();
403424
return selectedOption.viewValue;
@@ -422,18 +443,22 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
422443
if (value) {
423444
value.forEach(function(item) {
424445
var option = options.getOptionFromViewValue(item);
425-
if (option) option.element.selected = true;
446+
if (option && !option.disabled) option.element.selected = true;
426447
});
427448
}
428449
};
429450

430451

431452
selectCtrl.readValue = function readNgOptionsMultiple() {
432-
var selectedValues = selectElement.val() || [];
433-
return selectedValues.map(function(selectedKey) {
434-
var option = options.selectValueMap[selectedKey];
435-
return option.viewValue;
453+
var selectedValues = selectElement.val() || [],
454+
selections = [];
455+
456+
forEach(selectedValues, function(value) {
457+
var option = options.selectValueMap[value];
458+
if (!option.disabled) selections.push(option.viewValue);
436459
});
460+
461+
return selections;
437462
};
438463
}
439464

@@ -466,6 +491,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
466491

467492
function updateOptionElement(option, element) {
468493
option.element = element;
494+
element.disabled = option.disabled;
469495
if (option.value !== element.value) element.value = option.selectValue;
470496
if (option.label !== element.label) {
471497
element.label = option.label;

test/ng/directive/ngOptionsSpec.js

+107
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,57 @@ describe('ngOptions', function() {
532532
expect(options.eq(3)).toEqualOption('c');
533533
});
534534

535+
it('should disable options', function() {
536+
537+
scope.selected = '';
538+
scope.options = [
539+
{ name: 'white', value: '#FFFFFF' },
540+
{ name: 'one', value: 1, unavailable: true },
541+
{ name: 'notTrue', value: false },
542+
{ name: 'thirty', value: 30, unavailable: false }
543+
];
544+
createSelect({
545+
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
546+
'ng-model': 'selected'
547+
});
548+
var options = element.find('option');
549+
550+
expect(options.length).toEqual(5);
551+
expect(options.eq(1).prop('disabled')).toEqual(false);
552+
expect(options.eq(2).prop('disabled')).toEqual(true);
553+
expect(options.eq(3).prop('disabled')).toEqual(false);
554+
expect(options.eq(4).prop('disabled')).toEqual(false);
555+
});
556+
557+
it('should not write disabled options from model', function() {
558+
scope.selected = 30;
559+
scope.options = [
560+
{ name: 'white', value: '#FFFFFF' },
561+
{ name: 'one', value: 1, unavailable: true },
562+
{ name: 'notTrue', value: false },
563+
{ name: 'thirty', value: 30, unavailable: false }
564+
];
565+
createSelect({
566+
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
567+
'ng-model': 'selected'
568+
});
569+
570+
var options = element.find('option');
571+
572+
expect(options.eq(3).prop('selected')).toEqual(true);
573+
574+
scope.$apply(function() {
575+
scope.selected = 1;
576+
});
577+
578+
options = element.find('option');
579+
580+
expect(element.val()).toEqualUnknownValue('?');
581+
expect(options.length).toEqual(5);
582+
expect(options.eq(0).prop('selected')).toEqual(true);
583+
expect(options.eq(2).prop('selected')).toEqual(false);
584+
expect(options.eq(4).prop('selected')).toEqual(false);
585+
});
535586

536587
describe('selectAs expression', function() {
537588
beforeEach(function() {
@@ -1164,6 +1215,31 @@ describe('ngOptions', function() {
11641215
expect(element).toEqualSelectValue(scope.selected);
11651216
});
11661217

1218+
it('should bind to object disabled', function() {
1219+
scope.selected = 30;
1220+
scope.options = [
1221+
{ name: 'white', value: '#FFFFFF' },
1222+
{ name: 'one', value: 1, unavailable: true },
1223+
{ name: 'notTrue', value: false },
1224+
{ name: 'thirty', value: 30, unavailable: false }
1225+
];
1226+
createSelect({
1227+
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
1228+
'ng-model': 'selected'
1229+
});
1230+
1231+
var options = element.find('option');
1232+
1233+
expect(scope.options[1].unavailable).toEqual(true);
1234+
expect(options.eq(1).prop('disabled')).toEqual(true);
1235+
1236+
scope.$apply(function() {
1237+
scope.options[1].unavailable = false;
1238+
});
1239+
1240+
expect(scope.options[1].unavailable).toEqual(false);
1241+
expect(options.eq(1).prop('disabled')).toEqual(false);
1242+
});
11671243

11681244
it('should insert a blank option if bound to null', function() {
11691245
createSingleSelect();
@@ -1653,6 +1729,37 @@ describe('ngOptions', function() {
16531729
expect(element.find('option')[1].selected).toBeTruthy();
16541730
});
16551731

1732+
it('should not write disabled selections from model', function() {
1733+
scope.selected = [30];
1734+
scope.options = [
1735+
{ name: 'white', value: '#FFFFFF' },
1736+
{ name: 'one', value: 1, unavailable: true },
1737+
{ name: 'notTrue', value: false },
1738+
{ name: 'thirty', value: 30, unavailable: false }
1739+
];
1740+
createSelect({
1741+
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
1742+
'ng-model': 'selected',
1743+
'multiple': true
1744+
});
1745+
1746+
var options = element.find('option');
1747+
1748+
expect(options.eq(0).prop('selected')).toEqual(false);
1749+
expect(options.eq(1).prop('selected')).toEqual(false);
1750+
expect(options.eq(2).prop('selected')).toEqual(false);
1751+
expect(options.eq(3).prop('selected')).toEqual(true);
1752+
1753+
scope.$apply(function() {
1754+
scope.selected.push(1);
1755+
});
1756+
1757+
expect(options.eq(0).prop('selected')).toEqual(false);
1758+
expect(options.eq(1).prop('selected')).toEqual(false);
1759+
expect(options.eq(2).prop('selected')).toEqual(false);
1760+
expect(options.eq(3).prop('selected')).toEqual(true);
1761+
});
1762+
16561763

16571764
it('should update model on change', function() {
16581765
createMultiSelect();

0 commit comments

Comments
 (0)