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

Commit 20e79db

Browse files
committed
fix(input): register builtin parsers/formatters before anyone else
Previously, builtin parsers/formatters for e.g. `input[date]` or `input[number]` were added in the post linking phase to `ngModelController`, which in most cases was after a custom formatter/parser was registered. This commit registers builtin parsers/formatters already in the pre linking phase. With that builtin parsers run first, and builtin formatters run last. Closes #9218
1 parent b9479ee commit 20e79db

File tree

2 files changed

+99
-9
lines changed

2 files changed

+99
-9
lines changed

src/ng/directive/input.js

+21-9
Original file line numberDiff line numberDiff line change
@@ -1091,16 +1091,15 @@ function createDateInputType(type, regexp, parseDate, format) {
10911091
badInputChecker(scope, element, attr, ctrl);
10921092
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
10931093
var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
1094+
var previousDate;
10941095

10951096
ctrl.$$parserName = type;
10961097
ctrl.$parsers.push(function(value) {
10971098
if (ctrl.$isEmpty(value)) return null;
10981099
if (regexp.test(value)) {
1099-
var previousDate = ctrl.$modelValue;
1100-
if (previousDate && timezone === 'UTC') {
1101-
var timezoneOffset = 60000 * previousDate.getTimezoneOffset();
1102-
previousDate = new Date(previousDate.getTime() + timezoneOffset);
1103-
}
1100+
// Note: We cannot read ctrl.$modelValue, as there might be a different
1101+
// parser/formatter in the processing chain so that the model
1102+
// contains some different data format!
11041103
var parsedDate = parseDate(value, previousDate);
11051104
if (timezone === 'UTC') {
11061105
parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset());
@@ -1112,7 +1111,14 @@ function createDateInputType(type, regexp, parseDate, format) {
11121111

11131112
ctrl.$formatters.push(function(value) {
11141113
if (isDate(value)) {
1114+
previousDate = value;
1115+
if (previousDate && timezone === 'UTC') {
1116+
var timezoneOffset = 60000 * previousDate.getTimezoneOffset();
1117+
previousDate = new Date(previousDate.getTime() + timezoneOffset);
1118+
}
11151119
return $filter('date')(value, format, timezone);
1120+
} else {
1121+
previousDate = null;
11161122
}
11171123
return '';
11181124
});
@@ -1452,10 +1458,12 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
14521458
return {
14531459
restrict: 'E',
14541460
require: ['?ngModel'],
1455-
link: function(scope, element, attr, ctrls) {
1456-
if (ctrls[0]) {
1457-
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
1458-
$browser, $filter, $parse);
1461+
link: {
1462+
pre: function(scope, element, attr, ctrls) {
1463+
if (ctrls[0]) {
1464+
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
1465+
$browser, $filter, $parse);
1466+
}
14591467
}
14601468
}
14611469
};
@@ -2380,6 +2388,10 @@ var ngModelDirective = function() {
23802388
restrict: 'A',
23812389
require: ['ngModel', '^?form', '^?ngModelOptions'],
23822390
controller: NgModelController,
2391+
// Prelink needs to run before any input directive
2392+
// so that we can set the NgModelOptions in NgModelController
2393+
// before anyone else uses it.
2394+
priority: 1,
23832395
link: {
23842396
pre: function(scope, element, attr, ctrls) {
23852397
var modelCtrl = ctrls[0],

test/ng/directive/inputSpec.js

+78
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,84 @@ describe('ngModel', function() {
946946
dealoc(element);
947947
}));
948948

949+
describe('custom formatter and parser that are added by a directive in post linking', function() {
950+
beforeEach(module(function($compileProvider) {
951+
$compileProvider.directive('customFormat', function() {
952+
return {
953+
require: 'ngModel',
954+
link: function(scope, element, attrs, ngModelCtrl) {
955+
ngModelCtrl.$formatters.push(function(value) {
956+
return value.part;
957+
});
958+
ngModelCtrl.$parsers.push(function(value) {
959+
return {part: value};
960+
});
961+
}
962+
};
963+
});
964+
}));
965+
966+
function run(options) {
967+
inject(function($compile, $rootScope, $sniffer) {
968+
969+
var inputElm = $compile('<input type="'+options.type+'" ng-model="val" custom-format/>')($rootScope);
970+
971+
$rootScope.val = {part: options.modelToView.model};
972+
$rootScope.$digest();
973+
expect(inputElm.val()).toBe(options.modelToView.view);
974+
975+
inputElm.val(options.viewToModel.view);
976+
browserTrigger(inputElm, 'change');
977+
expect($rootScope.val).toEqual({part: options.viewToModel.model});
978+
979+
dealoc(inputElm);
980+
});
981+
}
982+
983+
it('should use them after the builtin ones for text inputs', function() {
984+
run({
985+
type: 'text',
986+
modelToView: {
987+
model: 'a',
988+
view: 'a'
989+
},
990+
viewToModel: {
991+
view: 'b',
992+
model: 'b'
993+
}
994+
});
995+
});
996+
997+
it('should use them after the builtin ones for number inputs', function() {
998+
run({
999+
type: 'number',
1000+
modelToView: {
1001+
model: 1,
1002+
view: '1'
1003+
},
1004+
viewToModel: {
1005+
view: '2',
1006+
model: 2
1007+
}
1008+
});
1009+
});
1010+
1011+
it('should use them after the builtin ones for date inputs', function() {
1012+
run({
1013+
type: 'date',
1014+
modelToView: {
1015+
model: new Date(2000, 10, 8),
1016+
view: '2000-11-08'
1017+
},
1018+
viewToModel: {
1019+
view: '2001-12-09',
1020+
model: new Date(2001, 11, 9)
1021+
}
1022+
});
1023+
});
1024+
});
1025+
1026+
9491027
it('should always format the viewValue as a string for a blank input type when the value is present',
9501028
inject(function($compile, $rootScope, $sniffer) {
9511029

0 commit comments

Comments
 (0)