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

Commit 529b250

Browse files
Narretzpetebacondarwin
authored andcommittedJan 7, 2016
fix(select): re-define ngModelCtrl.$render in the select postLink fn
Previously, the `$render` function was re-defined in the `select` directive's `preLink` function. When a `select` element is compiled, every `option` element inside it is linked and registered with the `selectCtrl`, which calls `$render` to update the selected `option`. `$render` calls `selectCtrl.writeValue`, which adds an unknown `option` in case no option is selected. In cases where `optgroup` elements are followed by a line-break, adding the unknown `option` confuses the html compiler and makes it call the link function of the following `option` with a wrong element, which means this option is not correctly registered. Since manipulation of the DOM in the `preLink` function is wrong API usage, the problem cannot be fixed in the compiler. With this commit, the `$render` function is not re-defined until the `select` directive's `postLink` function, at which point all `option` elements have been linked already. The commit also changes the `toEqualSelectWithOptions` matcher to take selected options in groups into account. Closes #13583 Closes #13583 Closes #13663
1 parent 6d19aa2 commit 529b250

File tree

2 files changed

+106
-33
lines changed

2 files changed

+106
-33
lines changed
 

‎src/ng/directive/select.js

+19-8
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,8 @@ var selectDirective = function() {
356356
controller: SelectController,
357357
priority: 1,
358358
link: {
359-
pre: selectPreLink
359+
pre: selectPreLink,
360+
post: selectPostLink
360361
}
361362
};
362363

@@ -370,13 +371,6 @@ var selectDirective = function() {
370371

371372
selectCtrl.ngModelCtrl = ngModelCtrl;
372373

373-
// We delegate rendering to the `writeValue` method, which can be changed
374-
// if the select can have multiple selected values or if the options are being
375-
// generated by `ngOptions`
376-
ngModelCtrl.$render = function() {
377-
selectCtrl.writeValue(ngModelCtrl.$viewValue);
378-
};
379-
380374
// When the selected item(s) changes we delegate getting the value of the select control
381375
// to the `readValue` method, which can be changed if the select can have multiple
382376
// selected values or if the options are being generated by `ngOptions`
@@ -430,6 +424,23 @@ var selectDirective = function() {
430424

431425
}
432426
}
427+
428+
function selectPostLink(scope, element, attrs, ctrls) {
429+
// if ngModel is not defined, we don't need to do anything
430+
var ngModelCtrl = ctrls[1];
431+
if (!ngModelCtrl) return;
432+
433+
var selectCtrl = ctrls[0];
434+
435+
// We delegate rendering to the `writeValue` method, which can be changed
436+
// if the select can have multiple selected values or if the options are being
437+
// generated by `ngOptions`.
438+
// This must be done in the postLink fn to prevent $render to be called before
439+
// all nodes have been linked correctly.
440+
ngModelCtrl.$render = function() {
441+
selectCtrl.writeValue(ngModelCtrl.$viewValue);
442+
};
443+
}
433444
};
434445

435446

‎test/ng/directive/selectSpec.js

+87-25
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
describe('select', function() {
4-
var scope, formElement, element, $compile;
4+
var scope, formElement, element, $compile, ngModelCtrl, selectCtrl, renderSpy;
55

66
function compile(html) {
77
formElement = jqLite('<form name="form">' + html + '</form>');
@@ -10,10 +10,42 @@ describe('select', function() {
1010
scope.$apply();
1111
}
1212

13+
function compileRepeatedOptions() {
14+
compile('<select ng-model="robot">' +
15+
'<option value="{{item.value}}" ng-repeat="item in robots">{{item.label}}</option>' +
16+
'</select>');
17+
}
18+
19+
function compileGroupedOptions() {
20+
compile(
21+
'<select ng-model="mySelect">' +
22+
'<option ng-repeat="item in values">{{item.name}}</option>' +
23+
'<optgroup ng-repeat="group in groups" label="{{group.name}}">' +
24+
'<option ng-repeat="item in group.values">{{item.name}}</option>' +
25+
'</optgroup>' +
26+
'</select>');
27+
}
28+
1329
function unknownValue(value) {
1430
return '? ' + hashKey(value) + ' ?';
1531
}
1632

33+
beforeEach(module(function($compileProvider) {
34+
$compileProvider.directive('spyOnWriteValue', function() {
35+
return {
36+
require: 'select',
37+
link: {
38+
pre: function(scope, element, attrs, ctrl) {
39+
selectCtrl = ctrl;
40+
renderSpy = jasmine.createSpy('renderSpy');
41+
selectCtrl.ngModelCtrl.$render = renderSpy.andCallFake(selectCtrl.ngModelCtrl.$render);
42+
spyOn(selectCtrl, 'writeValue').andCallThrough();
43+
}
44+
}
45+
};
46+
});
47+
}));
48+
1749
beforeEach(inject(function($rootScope, _$compile_) {
1850
scope = $rootScope.$new(); //create a child scope because the root scope can't be $destroy-ed
1951
$compile = _$compile_;
@@ -47,12 +79,14 @@ describe('select', function() {
4779
toEqualSelectWithOptions: function(expected) {
4880
var actualValues = {};
4981
var optionGroup;
82+
var optionValue;
5083

5184
forEach(this.actual.find('option'), function(option) {
5285
optionGroup = option.parentNode.label || '';
5386
actualValues[optionGroup] = actualValues[optionGroup] || [];
5487
// IE9 doesn't populate the label property from the text property like other browsers
55-
actualValues[optionGroup].push(option.label || option.text);
88+
optionValue = option.label || option.text;
89+
actualValues[optionGroup].push(option.selected ? [optionValue] : optionValue);
5690
});
5791

5892
this.message = function() {
@@ -198,6 +232,50 @@ describe('select', function() {
198232
});
199233

200234

235+
it('should select options in a group when there is a linebreak before an option', function() {
236+
scope.mySelect = 'B';
237+
scope.$apply();
238+
239+
var select = jqLite(
240+
'<select ng-model="mySelect">' +
241+
'<optgroup label="first">' +
242+
'<option value="A">A</option>' +
243+
'</optgroup>' +
244+
'<optgroup label="second">' + '\n' +
245+
'<option value="B">B</option>' +
246+
'</optgroup> ' +
247+
'</select>');
248+
249+
$compile(select)(scope);
250+
scope.$apply();
251+
252+
expect(select).toEqualSelectWithOptions({'first':['A'], 'second': [['B']]});
253+
dealoc(select);
254+
});
255+
256+
257+
it('should only call selectCtrl.writeValue after a digest has occured', function() {
258+
scope.mySelect = 'B';
259+
scope.$apply();
260+
261+
var select = jqLite(
262+
'<select spy-on-write-value ng-model="mySelect">' +
263+
'<optgroup label="first">' +
264+
'<option value="A">A</option>' +
265+
'</optgroup>' +
266+
'<optgroup label="second">' + '\n' +
267+
'<option value="B">B</option>' +
268+
'</optgroup> ' +
269+
'</select>');
270+
271+
$compile(select)(scope);
272+
expect(selectCtrl.writeValue).not.toHaveBeenCalled();
273+
274+
scope.$digest();
275+
expect(selectCtrl.writeValue).toHaveBeenCalledOnce();
276+
dealoc(select);
277+
});
278+
201279
describe('empty option', function() {
202280

203281
it('should allow empty option to be added and removed dynamically', function() {
@@ -538,22 +616,6 @@ describe('select', function() {
538616

539617
describe('selectController.hasOption', function() {
540618

541-
function compileRepeatedOptions() {
542-
compile('<select ng-model="robot">' +
543-
'<option value="{{item.value}}" ng-repeat="item in robots">{{item.label}}</option>' +
544-
'</select>');
545-
}
546-
547-
function compileGroupedOptions() {
548-
compile(
549-
'<select ng-model="mySelect">' +
550-
'<option ng-repeat="item in values">{{item.name}}</option>' +
551-
'<optgroup ng-repeat="group in groups" label="{{group.name}}">' +
552-
'<option ng-repeat="item in group.values">{{item.name}}</option>' +
553-
'</optgroup>' +
554-
'</select>');
555-
}
556-
557619
describe('flat options', function() {
558620
it('should return false for options shifted via ngRepeat', function() {
559621
scope.robots = [
@@ -624,7 +686,7 @@ describe('select', function() {
624686
expect(selectCtrl.hasOption('A')).toBe(true);
625687
expect(selectCtrl.hasOption('B')).toBe(true);
626688
expect(selectCtrl.hasOption('C')).toBe(true);
627-
expect(element).toEqualSelectWithOptions({'': ['A', 'B', 'C']});
689+
expect(element).toEqualSelectWithOptions({'': ['A', 'B', ['C']]});
628690
});
629691
});
630692

@@ -665,7 +727,7 @@ describe('select', function() {
665727
expect(selectCtrl.hasOption('C')).toBe(true);
666728
expect(selectCtrl.hasOption('D')).toBe(true);
667729
expect(selectCtrl.hasOption('E')).toBe(true);
668-
expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C'], 'second': ['D', 'E']});
730+
expect(element).toEqualSelectWithOptions({'': [['']], 'first':['B', 'C'], 'second': ['D', 'E']});
669731
});
670732

671733

@@ -702,7 +764,7 @@ describe('select', function() {
702764
expect(selectCtrl.hasOption('C')).toBe(true);
703765
expect(selectCtrl.hasOption('D')).toBe(true);
704766
expect(selectCtrl.hasOption('E')).toBe(true);
705-
expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C', 'D'], 'second': ['E']});
767+
expect(element).toEqualSelectWithOptions({'': [['']], 'first':['B', 'C', 'D'], 'second': ['E']});
706768
});
707769

708770

@@ -741,7 +803,7 @@ describe('select', function() {
741803
expect(selectCtrl.hasOption('D')).toBe(true);
742804
expect(selectCtrl.hasOption('E')).toBe(true);
743805
expect(selectCtrl.hasOption('F')).toBe(false);
744-
expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['D', 'E']});
806+
expect(element).toEqualSelectWithOptions({'': [[''], 'A'], 'first':['B', 'C'], 'second': ['D', 'E']});
745807
});
746808

747809

@@ -778,7 +840,7 @@ describe('select', function() {
778840
expect(selectCtrl.hasOption('C')).toBe(false);
779841
expect(selectCtrl.hasOption('D')).toBe(true);
780842
expect(selectCtrl.hasOption('E')).toBe(true);
781-
expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'D'], 'second': ['E']});
843+
expect(element).toEqualSelectWithOptions({'': [[''], 'A'], 'first':['B', 'D'], 'second': ['E']});
782844
});
783845

784846

@@ -813,7 +875,7 @@ describe('select', function() {
813875
expect(selectCtrl.hasOption('C')).toBe(true);
814876
expect(selectCtrl.hasOption('D')).toBe(false);
815877
expect(selectCtrl.hasOption('E')).toBe(true);
816-
expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['E']});
878+
expect(element).toEqualSelectWithOptions({'': [[''], 'A'], 'first':['B', 'C'], 'second': ['E']});
817879
});
818880

819881

@@ -848,7 +910,7 @@ describe('select', function() {
848910
expect(selectCtrl.hasOption('C')).toBe(true);
849911
expect(selectCtrl.hasOption('D')).toBe(false);
850912
expect(selectCtrl.hasOption('E')).toBe(false);
851-
expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C']});
913+
expect(element).toEqualSelectWithOptions({'': [[''], 'A'], 'first':['B', 'C']});
852914
});
853915
});
854916
});

0 commit comments

Comments
 (0)
This repository has been archived.