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

Commit 77ae42d

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 c1199fb commit 77ae42d

File tree

2 files changed

+154
-24
lines changed

2 files changed

+154
-24
lines changed

src/ng/directive/ngOptions.js

+47-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
*
@@ -129,10 +134,10 @@ var ngOptionsMinErr = minErr('ngOptions');
129134
.controller('ExampleController', ['$scope', function($scope) {
130135
$scope.colors = [
131136
{name:'black', shade:'dark'},
132-
{name:'white', shade:'light'},
137+
{name:'white', shade:'light', notAnOption: true},
133138
{name:'red', shade:'dark'},
134-
{name:'blue', shade:'dark'},
135-
{name:'yellow', shade:'light'}
139+
{name:'blue', shade:'dark', notAnOption: true},
140+
{name:'yellow', shade:'light', notAnOption: false}
136141
];
137142
$scope.myColor = $scope.colors[2]; // red
138143
}]);
@@ -141,6 +146,7 @@ var ngOptionsMinErr = minErr('ngOptions');
141146
<ul>
142147
<li ng-repeat="color in colors">
143148
Name: <input ng-model="color.name">
149+
<input type="checkbox" ng-model="color.notAnOption"> Disabled?
144150
[<a href ng-click="colors.splice($index, 1)">X</a>]
145151
</li>
146152
<li>
@@ -162,6 +168,12 @@ var ngOptionsMinErr = minErr('ngOptions');
162168
<select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
163169
</select><br/>
164170
171+
Color grouped by shade, with some disabled:
172+
<select ng-model="myColor"
173+
ng-options="color.name group by color.shade disable by color.notAnOption for color in colors">
174+
</select><br/>
175+
176+
165177
166178
Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br>
167179
<hr/>
@@ -186,16 +198,17 @@ var ngOptionsMinErr = minErr('ngOptions');
186198
*/
187199

188200
// 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]+?))?$/;
201+
// //000011111111110000000000022222222220000000000000000000003333333333000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999
202+
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]+?))?$/;
191203
// 1: value expression (valueFn)
192204
// 2: label expression (displayFn)
193205
// 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
206+
// 4: disable by expression (disableByFn)
207+
// 5: array item variable name
208+
// 6: object item key variable name
209+
// 7: object item value variable name
210+
// 8: collection expression
211+
// 9: track by expression
199212
// jshint maxlen: 100
200213

201214

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

217230
// The variable name for the value of the item in the collection
218-
var valueName = match[4] || match[6];
231+
var valueName = match[5] || match[7];
219232
// The variable name for the key of the item in the collection
220-
var keyName = match[5];
233+
var keyName = match[6];
221234

222235
// An expression that generates the viewValue for an option if there is a label expression
223236
var selectAs = / as /.test(match[0]) && match[1];
224237
// An expression that is used to track the id of each object in the options collection
225-
var trackBy = match[8];
238+
var trackBy = match[9];
226239
// An expression that generates the viewValue for an option if there is no label expression
227240
var valueFn = $parse(match[2] ? match[1] : valueName);
228241
var selectAsFn = selectAs && $parse(selectAs);
@@ -237,7 +250,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
237250
function getHashOfValue(viewValue) { return hashKey(viewValue); };
238251
var displayFn = $parse(match[2] || match[1]);
239252
var groupByFn = $parse(match[3] || '');
240-
var valuesFn = $parse(match[7]);
253+
var disableByFn = $parse(match[4] || '');
254+
var valuesFn = $parse(match[8]);
241255

242256
var locals = {};
243257
var getLocals = keyName ? function(value, key) {
@@ -250,11 +264,12 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
250264
};
251265

252266

253-
function Option(selectValue, viewValue, label, group) {
267+
function Option(selectValue, viewValue, label, group, disabled) {
254268
this.selectValue = selectValue;
255269
this.viewValue = viewValue;
256270
this.label = label;
257271
this.group = group;
272+
this.disabled = disabled;
258273
}
259274

260275
return {
@@ -269,8 +284,10 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
269284
var locals = getLocals(values[key], key);
270285
var label = displayFn(scope, locals);
271286
var selectValue = getTrackByValue(values[key], locals);
287+
var disabledBy = disableByFn(scope, locals);
272288
watchedArray.push(selectValue);
273289
watchedArray.push(label);
290+
watchedArray.push(disabledBy);
274291
});
275292
return watchedArray;
276293
}),
@@ -296,7 +313,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
296313
var selectValue = getTrackByValue(viewValue, locals);
297314
var label = displayFn(scope, locals);
298315
var group = groupByFn(scope, locals);
299-
var optionItem = new Option(selectValue, viewValue, label, group);
316+
var disabled = disableByFn(scope, locals);
317+
var optionItem = new Option(selectValue, viewValue, label, group, disabled);
300318

301319
optionItems.push(optionItem);
302320
selectValueMap[selectValue] = optionItem;
@@ -322,7 +340,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
322340
return {
323341
restrict: 'A',
324342
terminal: true,
325-
require: ['select', '?ngModel'],
343+
require: ['select', 'ngModel'],
326344
link: function(scope, selectElement, attr, ctrls) {
327345

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

376-
if (option) {
394+
if (option && !option.disabled) {
377395
if (selectElement[0].value !== option.selectValue) {
378396
removeUnknownOption();
379397
removeEmptyOption();
@@ -397,7 +415,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
397415

398416
var selectedOption = options.selectValueMap[selectElement.val()];
399417

400-
if (selectedOption) {
418+
if (selectedOption && !selectedOption.disabled) {
401419
removeEmptyOption();
402420
removeUnknownOption();
403421
return selectedOption.viewValue;
@@ -422,18 +440,22 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
422440
if (value) {
423441
value.forEach(function(item) {
424442
var option = options.getOptionFromViewValue(item);
425-
if (option) option.element.selected = true;
443+
if (option && !option.disabled) option.element.selected = true;
426444
});
427445
}
428446
};
429447

430448

431449
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;
450+
var selectedValues = selectElement.val() || [],
451+
selections = [];
452+
453+
forEach(selectedValues, function(value) {
454+
var option = options.selectValueMap[value];
455+
if (!option.disabled) selections.push(option.viewValue);
436456
});
457+
458+
return selections;
437459
};
438460
}
439461

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

467489
function updateOptionElement(option, element) {
468490
option.element = element;
491+
element.disabled = option.disabled;
469492
if (option.value !== element.value) element.value = option.selectValue;
470493
if (option.label !== element.label) {
471494
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)