Skip to content

Commit

Permalink
fix(input): register builtin parsers/formatters before anyone else
Browse files Browse the repository at this point in the history
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 angular#9218
Closes angular#9358
  • Loading branch information
tbosch committed Oct 2, 2014
1 parent a0bfdd0 commit 1064443
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 9 deletions.
30 changes: 21 additions & 9 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -1115,7 +1114,14 @@ function createDateInputType(type, regexp, parseDate, format) {
if (!isDate(value)) {
throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', 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 '';
});
Expand Down Expand Up @@ -1460,10 +1466,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);
}
}
}
};
Expand Down Expand Up @@ -2383,6 +2391,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,
compile: function ngModelCompile(element) {
// Setup initial state of the control
element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS);
Expand Down
63 changes: 63 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<input type="'+type+'" ng-model="val" custom-format/>')($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) {

Expand Down

0 comments on commit 1064443

Please sign in to comment.