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

fix(select): re-define ngModelCtrl.$render in the select postLink fn #13663

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/ng/directive/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,8 @@ var selectDirective = function() {
controller: SelectController,
priority: 1,
link: {
pre: selectPreLink
pre: selectPreLink,
post: selectPostLink
}
};

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

selectCtrl.ngModelCtrl = ngModelCtrl;

// We delegate rendering to the `writeValue` method, which can be changed
// if the select can have multiple selected values or if the options are being
// generated by `ngOptions`
ngModelCtrl.$render = function() {
selectCtrl.writeValue(ngModelCtrl.$viewValue);
};

// When the selected item(s) changes we delegate getting the value of the select control
// to the `readValue` method, which can be changed if the select can have multiple
// selected values or if the options are being generated by `ngOptions`
Expand Down Expand Up @@ -430,6 +424,23 @@ var selectDirective = function() {

}
}

function selectPostLink(scope, element, attrs, ctrls) {
// if ngModel is not defined, we don't need to do anything
var ngModelCtrl = ctrls[1];
if (!ngModelCtrl) return;

var selectCtrl = ctrls[0];

// We delegate rendering to the `writeValue` method, which can be changed
// if the select can have multiple selected values or if the options are being
// generated by `ngOptions`.
// This must be done in the postLink fn to prevent $render to be called before
// all nodes have been linked correctly.
ngModelCtrl.$render = function() {
selectCtrl.writeValue(ngModelCtrl.$viewValue);
};
}
};


Expand Down
112 changes: 87 additions & 25 deletions test/ng/directive/selectSpec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

describe('select', function() {
var scope, formElement, element, $compile;
var scope, formElement, element, $compile, ngModelCtrl, selectCtrl, renderSpy;

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

function compileRepeatedOptions() {
compile('<select ng-model="robot">' +
'<option value="{{item.value}}" ng-repeat="item in robots">{{item.label}}</option>' +
'</select>');
}

function compileGroupedOptions() {
compile(
'<select ng-model="mySelect">' +
'<option ng-repeat="item in values">{{item.name}}</option>' +
'<optgroup ng-repeat="group in groups" label="{{group.name}}">' +
'<option ng-repeat="item in group.values">{{item.name}}</option>' +
'</optgroup>' +
'</select>');
}

function unknownValue(value) {
return '? ' + hashKey(value) + ' ?';
}

beforeEach(module(function($compileProvider) {
$compileProvider.directive('spyOnWriteValue', function() {
return {
require: 'select',
link: {
pre: function(scope, element, attrs, ctrl) {
selectCtrl = ctrl;
renderSpy = jasmine.createSpy('renderSpy');
selectCtrl.ngModelCtrl.$render = renderSpy.andCallFake(selectCtrl.ngModelCtrl.$render);
spyOn(selectCtrl, 'writeValue').andCallThrough();
}
}
};
});
}));

beforeEach(inject(function($rootScope, _$compile_) {
scope = $rootScope.$new(); //create a child scope because the root scope can't be $destroy-ed
$compile = _$compile_;
Expand Down Expand Up @@ -47,12 +79,14 @@ describe('select', function() {
toEqualSelectWithOptions: function(expected) {
var actualValues = {};
var optionGroup;
var optionValue;

forEach(this.actual.find('option'), function(option) {
optionGroup = option.parentNode.label || '';
actualValues[optionGroup] = actualValues[optionGroup] || [];
// IE9 doesn't populate the label property from the text property like other browsers
actualValues[optionGroup].push(option.label || option.text);
optionValue = option.label || option.text;
actualValues[optionGroup].push(option.selected ? [optionValue] : optionValue);
});

this.message = function() {
Expand Down Expand Up @@ -198,6 +232,50 @@ describe('select', function() {
});


it('should select options in a group when there is a linebreak before an option', function() {
scope.mySelect = 'B';
scope.$apply();

var select = jqLite(
'<select ng-model="mySelect">' +
'<optgroup label="first">' +
'<option value="A">A</option>' +
'</optgroup>' +
'<optgroup label="second">' + '\n' +
'<option value="B">B</option>' +
'</optgroup> ' +
'</select>');

$compile(select)(scope);
scope.$apply();

expect(select).toEqualSelectWithOptions({'first':['A'], 'second': [['B']]});
dealoc(select);
});


it('should only call selectCtrl.writeValue after a digest has occured', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tests the behavior explicitly, while the first test only tests the functionality. I like this one though, because it makes sure we don't call writeValue for each linked option during compilation.

scope.mySelect = 'B';
scope.$apply();

var select = jqLite(
'<select spy-on-write-value ng-model="mySelect">' +
'<optgroup label="first">' +
'<option value="A">A</option>' +
'</optgroup>' +
'<optgroup label="second">' + '\n' +
'<option value="B">B</option>' +
'</optgroup> ' +
'</select>');

$compile(select)(scope);
expect(selectCtrl.writeValue).not.toHaveBeenCalled();

scope.$digest();
expect(selectCtrl.writeValue).toHaveBeenCalledOnce();
dealoc(select);
});

describe('empty option', function() {

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

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

function compileRepeatedOptions() {
compile('<select ng-model="robot">' +
'<option value="{{item.value}}" ng-repeat="item in robots">{{item.label}}</option>' +
'</select>');
}

function compileGroupedOptions() {
compile(
'<select ng-model="mySelect">' +
'<option ng-repeat="item in values">{{item.name}}</option>' +
'<optgroup ng-repeat="group in groups" label="{{group.name}}">' +
'<option ng-repeat="item in group.values">{{item.name}}</option>' +
'</optgroup>' +
'</select>');
}

describe('flat options', function() {
it('should return false for options shifted via ngRepeat', function() {
scope.robots = [
Expand Down Expand Up @@ -624,7 +686,7 @@ describe('select', function() {
expect(selectCtrl.hasOption('A')).toBe(true);
expect(selectCtrl.hasOption('B')).toBe(true);
expect(selectCtrl.hasOption('C')).toBe(true);
expect(element).toEqualSelectWithOptions({'': ['A', 'B', 'C']});
expect(element).toEqualSelectWithOptions({'': ['A', 'B', ['C']]});
});
});

Expand Down Expand Up @@ -665,7 +727,7 @@ describe('select', function() {
expect(selectCtrl.hasOption('C')).toBe(true);
expect(selectCtrl.hasOption('D')).toBe(true);
expect(selectCtrl.hasOption('E')).toBe(true);
expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C'], 'second': ['D', 'E']});
expect(element).toEqualSelectWithOptions({'': [['']], 'first':['B', 'C'], 'second': ['D', 'E']});
});


Expand Down Expand Up @@ -702,7 +764,7 @@ describe('select', function() {
expect(selectCtrl.hasOption('C')).toBe(true);
expect(selectCtrl.hasOption('D')).toBe(true);
expect(selectCtrl.hasOption('E')).toBe(true);
expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C', 'D'], 'second': ['E']});
expect(element).toEqualSelectWithOptions({'': [['']], 'first':['B', 'C', 'D'], 'second': ['E']});
});


Expand Down Expand Up @@ -741,7 +803,7 @@ describe('select', function() {
expect(selectCtrl.hasOption('D')).toBe(true);
expect(selectCtrl.hasOption('E')).toBe(true);
expect(selectCtrl.hasOption('F')).toBe(false);
expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['D', 'E']});
expect(element).toEqualSelectWithOptions({'': [[''], 'A'], 'first':['B', 'C'], 'second': ['D', 'E']});
});


Expand Down Expand Up @@ -778,7 +840,7 @@ describe('select', function() {
expect(selectCtrl.hasOption('C')).toBe(false);
expect(selectCtrl.hasOption('D')).toBe(true);
expect(selectCtrl.hasOption('E')).toBe(true);
expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'D'], 'second': ['E']});
expect(element).toEqualSelectWithOptions({'': [[''], 'A'], 'first':['B', 'D'], 'second': ['E']});
});


Expand Down Expand Up @@ -813,7 +875,7 @@ describe('select', function() {
expect(selectCtrl.hasOption('C')).toBe(true);
expect(selectCtrl.hasOption('D')).toBe(false);
expect(selectCtrl.hasOption('E')).toBe(true);
expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['E']});
expect(element).toEqualSelectWithOptions({'': [[''], 'A'], 'first':['B', 'C'], 'second': ['E']});
});


Expand Down Expand Up @@ -848,7 +910,7 @@ describe('select', function() {
expect(selectCtrl.hasOption('C')).toBe(true);
expect(selectCtrl.hasOption('D')).toBe(false);
expect(selectCtrl.hasOption('E')).toBe(false);
expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C']});
expect(element).toEqualSelectWithOptions({'': [[''], 'A'], 'first':['B', 'C']});
});
});
});
Expand Down