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

Commit 2435e2b

Browse files
shahatajeffbcross
authored andcommittedOct 9, 2014
fix(select): manage select controller options correctly
This fixes a regression that was introduced in 2bcd02d. Basically, the problem was that render() removed the wrong option from the select controller since it assumed that the option that was removed has the same label as the excessive option in existingOptions, but this is only correct if the option was popped from the end of the array. We now remember for each label whether it was added or removed (or removed at some point and then added at a different point) and report to the select controller only about options that were actually removed or added, ignoring any options that just moved. Closes #9418
1 parent 944408e commit 2435e2b

File tree

2 files changed

+100
-17
lines changed

2 files changed

+100
-17
lines changed
 

‎src/ng/directive/select.js

+30-3
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
200200
// Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
201201
// Adding an <option selected="selected"> element to a <select required="required"> should
202202
// automatically select the new element
203-
if (element[0].hasAttribute('selected')) {
203+
if (element && element[0].hasAttribute('selected')) {
204204
element[0].selected = true;
205205
}
206206
};
@@ -498,6 +498,23 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
498498
}
499499
}
500500

501+
/**
502+
* A new labelMap is created with each render.
503+
* This function is called for each existing option with added=false,
504+
* and each new option with added=true.
505+
* - Labels that are passed to this method twice,
506+
* (once with added=true and once with added=false) will end up with a value of 0, and
507+
* will cause no change to happen to the corresponding option.
508+
* - Labels that are passed to this method only once with added=false will end up with a
509+
* value of -1 and will eventually be passed to selectCtrl.removeOption()
510+
* - Labels that are passed to this method only once with added=true will end up with a
511+
* value of 1 and will eventually be passed to selectCtrl.addOption()
512+
*/
513+
function updateLabelMap(labelMap, label, added) {
514+
labelMap[label] = labelMap[label] || 0;
515+
labelMap[label] += (added ? 1 : -1);
516+
}
517+
501518
function render() {
502519
renderScheduled = false;
503520

@@ -515,6 +532,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
515532
value,
516533
groupLength, length,
517534
groupIndex, index,
535+
labelMap = {},
518536
selected,
519537
isSelected = createIsSelectedFn(viewValue),
520538
anySelected = false,
@@ -597,6 +615,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
597615
// reuse elements
598616
lastElement = existingOption.element;
599617
if (existingOption.label !== option.label) {
618+
updateLabelMap(labelMap, existingOption.label, false);
619+
updateLabelMap(labelMap, option.label, true);
600620
lastElement.text(existingOption.label = option.label);
601621
}
602622
if (existingOption.id !== option.id) {
@@ -636,7 +656,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
636656
id: option.id,
637657
selected: option.selected
638658
});
639-
selectCtrl.addOption(option.label, element);
659+
updateLabelMap(labelMap, option.label, true);
640660
if (lastElement) {
641661
lastElement.after(element);
642662
} else {
@@ -649,9 +669,16 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
649669
index++; // increment since the existingOptions[0] is parent element not OPTION
650670
while(existingOptions.length > index) {
651671
option = existingOptions.pop();
652-
selectCtrl.removeOption(option.label);
672+
updateLabelMap(labelMap, option.label, false);
653673
option.element.remove();
654674
}
675+
forEach(labelMap, function (count, label) {
676+
if (count > 0) {
677+
selectCtrl.addOption(label);
678+
} else if (count < 0) {
679+
selectCtrl.removeOption(label);
680+
}
681+
});
655682
}
656683
// remove any excessive OPTGROUPs from select
657684
while(optionGroupsCache.length > groupIndex) {

‎test/ng/directive/selectSpec.js

+70-14
Original file line numberDiff line numberDiff line change
@@ -407,31 +407,59 @@ describe('select', function() {
407407
});
408408

409409
describe('selectController.hasOption', function() {
410-
it('should return true for options added via ngOptions', function() {
410+
it('should return false for options shifted via ngOptions', function() {
411411
scope.robots = [
412-
{key: 1, value: 'c3p0'},
413-
{key: 2, value: 'r2d2'}
412+
{value: 1, label: 'c3p0'},
413+
{value: 2, label: 'r2d2'}
414414
];
415-
scope.robot = 'r2d2';
416415

417416
compile('<select ng-model="robot" ' +
418-
'ng-options="item.key as item.value for item in robots">' +
417+
'ng-options="item.value as item.label for item in robots">' +
419418
'</select>');
420419

421420
var selectCtrl = element.data().$selectController;
422421

423-
expect(selectCtrl.hasOption('c3p0')).toBe(true);
422+
scope.$apply(function() {
423+
scope.robots.shift();
424+
});
425+
426+
expect(selectCtrl.hasOption('c3p0')).toBe(false);
424427
expect(selectCtrl.hasOption('r2d2')).toBe(true);
428+
});
429+
430+
it('should return false for options popped via ngOptions', function() {
431+
scope.robots = [
432+
{value: 1, label: 'c3p0'},
433+
{value: 2, label: 'r2d2'}
434+
];
435+
436+
compile('<select ng-model="robot" ' +
437+
'ng-options="item.value as item.label for item in robots">' +
438+
'</select>');
439+
440+
var selectCtrl = element.data().$selectController;
425441

426442
scope.$apply(function() {
427443
scope.robots.pop();
428444
});
429445

430446
expect(selectCtrl.hasOption('c3p0')).toBe(true);
431447
expect(selectCtrl.hasOption('r2d2')).toBe(false);
448+
});
449+
450+
it('should return true for options added via ngOptions', function() {
451+
scope.robots = [
452+
{value: 2, label: 'r2d2'}
453+
];
454+
455+
compile('<select ng-model="robot" ' +
456+
'ng-options="item.value as item.label for item in robots">' +
457+
'</select>');
458+
459+
var selectCtrl = element.data().$selectController;
432460

433461
scope.$apply(function() {
434-
scope.robots.push({key: 2, value: 'r2d2'});
462+
scope.robots.unshift({value: 1, label: 'c3p0'});
435463
});
436464

437465
expect(selectCtrl.hasOption('c3p0')).toBe(true);
@@ -516,31 +544,59 @@ describe('select', function() {
516544
});
517545

518546
describe('selectController.hasOption', function() {
519-
it('should return true for options added via ngOptions', function() {
547+
it('should return false for options shifted via ngOptions', function() {
520548
scope.robots = [
521-
{key: 1, value: 'c3p0'},
522-
{key: 2, value: 'r2d2'}
549+
{value: 1, label: 'c3p0'},
550+
{value: 2, label: 'r2d2'}
523551
];
524-
scope.robot = 'r2d2';
525552

526553
compile('<select ng-model="robot" multiple ' +
527-
'ng-options="item.key as item.value for item in robots">' +
554+
'ng-options="item.value as item.label for item in robots">' +
528555
'</select>');
529556

530557
var selectCtrl = element.data().$selectController;
531558

532-
expect(selectCtrl.hasOption('c3p0')).toBe(true);
559+
scope.$apply(function() {
560+
scope.robots.shift();
561+
});
562+
563+
expect(selectCtrl.hasOption('c3p0')).toBe(false);
533564
expect(selectCtrl.hasOption('r2d2')).toBe(true);
565+
});
566+
567+
it('should return false for options popped via ngOptions', function() {
568+
scope.robots = [
569+
{value: 1, label: 'c3p0'},
570+
{value: 2, label: 'r2d2'}
571+
];
572+
573+
compile('<select ng-model="robot" multiple ' +
574+
'ng-options="item.value as item.label for item in robots">' +
575+
'</select>');
576+
577+
var selectCtrl = element.data().$selectController;
534578

535579
scope.$apply(function() {
536580
scope.robots.pop();
537581
});
538582

539583
expect(selectCtrl.hasOption('c3p0')).toBe(true);
540584
expect(selectCtrl.hasOption('r2d2')).toBe(false);
585+
});
586+
587+
it('should return true for options added via ngOptions', function() {
588+
scope.robots = [
589+
{value: 2, label: 'r2d2'}
590+
];
591+
592+
compile('<select ng-model="robot" multiple ' +
593+
'ng-options="item.value as item.label for item in robots">' +
594+
'</select>');
595+
596+
var selectCtrl = element.data().$selectController;
541597

542598
scope.$apply(function() {
543-
scope.robots.push({key: 2, value: 'r2d2'});
599+
scope.robots.unshift({value: 1, label: 'c3p0'});
544600
});
545601

546602
expect(selectCtrl.hasOption('c3p0')).toBe(true);

0 commit comments

Comments
 (0)
This repository has been archived.