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

Commit 4140250

Browse files
committed
fix(select): handle model updates when options are manipulated
These rules follow ngOptions behavior: - when an option that is currently selected, is removed or its value changes, the model is set to null. - when an an option is added or its value changes to match the currently selected model, this option is selected. - when an option is disabled, the model is set to null. - when the model value changes to a value that matches a disabled option, this option is selected (analogue to ngOptions)
1 parent 0e244cb commit 4140250

File tree

2 files changed

+804
-8
lines changed

2 files changed

+804
-8
lines changed

src/ng/directive/select.js

+61-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var SelectController =
2020

2121
// If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
2222
self.ngModelCtrl = noopNgModelController;
23+
self.multiple = false;
2324

2425
// The "unknown" option is one that is prepended to the list if the viewValue
2526
// does not match any of the options. When it is rendered the value of the unknown
@@ -91,7 +92,9 @@ var SelectController =
9192
}
9293
var count = optionsMap.get(value) || 0;
9394
optionsMap.put(value, count + 1);
94-
self.ngModelCtrl.$render();
95+
// Only render at the end of a digest. This improves render performance when many options
96+
// are added during a digest and ensures all relevant options are correctly marked as selected
97+
scheduleRender();
9598
};
9699

97100
// Tell the select control that an option, with the given value, has been removed
@@ -115,6 +118,15 @@ var SelectController =
115118
};
116119

117120

121+
var renderScheduled = false;
122+
function scheduleRender() {
123+
if (renderScheduled) return;
124+
renderScheduled = true;
125+
$scope.$$postDigest(function() {
126+
renderScheduled = false;
127+
self.ngModelCtrl.$render();
128+
});
129+
}
118130

119131
var updateScheduled = false;
120132
function scheduleViewValueUpdate(renderAfter) {
@@ -163,29 +175,74 @@ var SelectController =
163175
} else if (interpolateValueFn) {
164176
// The value attribute is interpolated
165177
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
178+
var currentVal = self.readValue();
179+
var removal;
180+
var previouslySelected = optionElement.prop('selected');
181+
var removedVal;
182+
166183
if (isDefined(oldVal)) {
167184
self.removeOption(oldVal);
185+
removal = true;
186+
removedVal = oldVal;
168187
}
169188
oldVal = newVal;
170189
self.addOption(newVal, optionElement);
190+
191+
if (removal && previouslySelected) {
192+
scheduleViewValueUpdate();
193+
}
171194
});
172195
} else if (interpolateTextFn) {
173196
// The text content is interpolated
174197
optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
175198
optionAttrs.$set('value', newVal);
199+
var previouslySelected = optionElement.prop('selected');
176200
if (oldVal !== newVal) {
177201
self.removeOption(oldVal);
178202
}
179203
self.addOption(newVal, optionElement);
204+
205+
if (oldVal && previouslySelected) {
206+
scheduleViewValueUpdate();
207+
}
180208
});
181209
} else {
182210
// The value attribute is static
183211
self.addOption(optionAttrs.value, optionElement);
184212
}
185213

214+
215+
var oldDisabled;
216+
optionAttrs.$observe('disabled', function(newVal) {
217+
218+
// Since model updates will also select disabled options (like ngOptions),
219+
// we only have to handle options becoming disabled, not enabled
220+
221+
if (newVal === 'true' || newVal && optionElement.prop('selected')) {
222+
if (self.multiple) {
223+
scheduleViewValueUpdate(true);
224+
} else {
225+
self.ngModelCtrl.$setViewValue(null);
226+
self.ngModelCtrl.$render();
227+
}
228+
oldDisabled = newVal;
229+
}
230+
});
231+
186232
optionElement.on('$destroy', function() {
187-
self.removeOption(optionAttrs.value);
233+
var currentValue = self.readValue();
234+
var removeValue = optionAttrs.value;
235+
236+
self.removeOption(removeValue);
188237
self.ngModelCtrl.$render();
238+
239+
if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 ||
240+
currentValue === removeValue
241+
) {
242+
// When multiple (selected) options are destroyed at the same time, we don't want
243+
// to run a model update for each of them. Instead, run a single update in the $$postDigest
244+
scheduleViewValueUpdate(true);
245+
}
189246
});
190247
};
191248
}];
@@ -477,12 +534,13 @@ var selectDirective = function() {
477534
// we have to add an extra watch since ngModel doesn't work well with arrays - it
478535
// doesn't trigger rendering if only an item in the array changes.
479536
if (attr.multiple) {
537+
selectCtrl.multiple = true;
480538

481539
// Read value now needs to check each option to see if it is selected
482540
selectCtrl.readValue = function readMultipleValue() {
483541
var array = [];
484542
forEach(element.find('option'), function(option) {
485-
if (option.selected) {
543+
if (option.selected && !option.disabled) {
486544
var val = option.value;
487545
array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val);
488546
}

0 commit comments

Comments
 (0)