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

Commit 5afa825

Browse files
committed
feat(ngOptions): add support for disabling an option
This patch adds support for disabling options based on model values. The "disable by" 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. closes #638
1 parent d6eba21 commit 5afa825

File tree

2 files changed

+156
-24
lines changed

2 files changed

+156
-24
lines changed

src/ng/directive/ngOptions.js

+49-24
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 by`** `disable` **`for`** `value` **`in`** `array`
9798
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
99+
* * `label` **`disable by`** `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 by`** `disable` **`for (`**`key`**`,`** `value`**`) in`** `object`
104107
* * `select` **`as`** `label` **`group by`** `group`
105108
* **`for` `(`**`key`**`,`** `value`**`) in`** `object`
109+
* * `select` **`as`** `label` **`disable by`** `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 by 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+
// //000011111111110000000000022222222220000000000000000000003333333333000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999
204+
var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\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]+?))?$/;
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 by expression (disableByFn)
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 disableByFn = $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,10 @@ 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 disabledBy = disableByFn(scope, locals);
272290
watchedArray.push(selectValue);
273291
watchedArray.push(label);
292+
watchedArray.push(disabledBy);
274293
});
275294
return watchedArray;
276295
}),
@@ -296,7 +315,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
296315
var selectValue = getTrackByValue(viewValue, locals);
297316
var label = displayFn(scope, locals);
298317
var group = groupByFn(scope, locals);
299-
var optionItem = new Option(selectValue, viewValue, label, group);
318+
var disabled = disableByFn(scope, locals);
319+
var optionItem = new Option(selectValue, viewValue, label, group, disabled);
300320

301321
optionItems.push(optionItem);
302322
selectValueMap[selectValue] = optionItem;
@@ -322,7 +342,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
322342
return {
323343
restrict: 'A',
324344
terminal: true,
325-
require: ['select', '?ngModel'],
345+
require: ['select', 'ngModel'],
326346
link: function(scope, selectElement, attr, ctrls) {
327347

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

376-
if (option) {
396+
if (option && !option.disabled) {
377397
if (selectElement[0].value !== option.selectValue) {
378398
removeUnknownOption();
379399
removeEmptyOption();
@@ -397,7 +417,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
397417

398418
var selectedOption = options.selectValueMap[selectElement.val()];
399419

400-
if (selectedOption) {
420+
if (selectedOption && !selectedOption.disabled) {
401421
removeEmptyOption();
402422
removeUnknownOption();
403423
return selectedOption.viewValue;
@@ -422,18 +442,22 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
422442
if (value) {
423443
value.forEach(function(item) {
424444
var option = options.getOptionFromViewValue(item);
425-
if (option) option.element.selected = true;
445+
if (option && !option.disabled) option.element.selected = true;
426446
});
427447
}
428448
};
429449

430450

431451
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;
452+
var selectedValues = selectElement.val() || [],
453+
selections = [];
454+
455+
forEach(selectedValues, function(value) {
456+
var option = options.selectValueMap[value];
457+
if (!option.disabled) selections.push(option.viewValue);
436458
});
459+
460+
return selections;
437461
};
438462
}
439463

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

467491
function updateOptionElement(option, element) {
468492
option.element = element;
493+
element.disabled = option.disabled;
469494
if (option.value !== element.value) element.value = option.selectValue;
470495
if (option.label !== element.label) {
471496
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 by 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 by 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 by 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 by 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)