From aab0b084966a657af4e7cd98358553f4b0de8b2a Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 30 Sep 2014 17:27:14 -0700 Subject: [PATCH] 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 Closes #9358 --- src/ng/directive/input.js | 30 +++++++++++----- test/ng/directive/inputSpec.js | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index b8ceea7e546a..4f7be46bff7a 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1091,16 +1091,15 @@ function createDateInputType(type, regexp, parseDate, format) { badInputChecker(scope, element, attr, ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var previousDate; ctrl.$$parserName = type; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; if (regexp.test(value)) { - var previousDate = ctrl.$modelValue; - if (previousDate && timezone === 'UTC') { - var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); - previousDate = new Date(previousDate.getTime() + timezoneOffset); - } + // Note: We cannot read ctrl.$modelValue, as there might be a different + // parser/formatter in the processing chain so that the model + // contains some different data format! var parsedDate = parseDate(value, previousDate); if (timezone === 'UTC') { parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); @@ -1112,7 +1111,14 @@ function createDateInputType(type, regexp, parseDate, format) { ctrl.$formatters.push(function(value) { if (isDate(value)) { + previousDate = value; + if (previousDate && timezone === 'UTC') { + var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); + previousDate = new Date(previousDate.getTime() + timezoneOffset); + } return $filter('date')(value, format, timezone); + } else { + previousDate = null; } return ''; }); @@ -1452,10 +1458,12 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', return { restrict: 'E', require: ['?ngModel'], - link: function(scope, element, attr, ctrls) { - if (ctrls[0]) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, - $browser, $filter, $parse); + link: { + pre: function(scope, element, attr, ctrls) { + if (ctrls[0]) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + $browser, $filter, $parse); + } } } }; @@ -2380,6 +2388,10 @@ var ngModelDirective = function() { restrict: 'A', require: ['ngModel', '^?form', '^?ngModelOptions'], controller: NgModelController, + // Prelink needs to run before any input directive + // so that we can set the NgModelOptions in NgModelController + // before anyone else uses it. + priority: 1, link: { pre: function(scope, element, attr, ctrls) { var modelCtrl = ctrls[0], diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index a10676732a14..52d5ca5266d8 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -946,6 +946,69 @@ describe('ngModel', function() { dealoc(element); })); + describe('custom formatter and parser that are added by a directive in post linking', function() { + var inputElm, scope; + beforeEach(module(function($compileProvider) { + $compileProvider.directive('customFormat', function() { + return { + require: 'ngModel', + link: function(scope, element, attrs, ngModelCtrl) { + ngModelCtrl.$formatters.push(function(value) { + return value.part; + }); + ngModelCtrl.$parsers.push(function(value) { + return {part: value}; + }); + } + }; + }); + })); + + afterEach(function() { + dealoc(inputElm); + }); + + function createInput(type) { + inject(function($compile, $rootScope) { + scope = $rootScope; + inputElm = $compile('')($rootScope); + }); + } + + it('should use them after the builtin ones for text inputs', function() { + createInput('text'); + scope.$apply('val = {part: "a"}'); + expect(inputElm.val()).toBe('a'); + + inputElm.val('b'); + browserTrigger(inputElm, 'change'); + expect(scope.val).toEqual({part: 'b'}); + }); + + it('should use them after the builtin ones for number inputs', function() { + createInput('number'); + scope.$apply('val = {part: 1}'); + expect(inputElm.val()).toBe('1'); + + inputElm.val('2'); + browserTrigger(inputElm, 'change'); + expect(scope.val).toEqual({part: 2}); + }); + + it('should use them after the builtin ones for date inputs', function() { + createInput('date'); + scope.$apply(function() { + scope.val = {part: new Date(2000, 10, 8)}; + }); + expect(inputElm.val()).toBe('2000-11-08'); + + inputElm.val('2001-12-09'); + browserTrigger(inputElm, 'change'); + expect(scope.val).toEqual({part: new Date(2001, 11, 9)}); + }); + }); + + it('should always format the viewValue as a string for a blank input type when the value is present', inject(function($compile, $rootScope, $sniffer) {