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

fix(ngModel, input): improve handling of built-in named parsers #16347

Merged
merged 1 commit into from
Dec 4, 2017
Merged
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
22 changes: 13 additions & 9 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -1429,12 +1429,11 @@ function createDateParser(regexp, mapping) {

function createDateInputType(type, regexp, parseDate, format) {
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
badInputChecker(scope, element, attr, ctrl);
badInputChecker(scope, element, attr, ctrl, type);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
var timezone = ctrl && ctrl.$options.getOption('timezone');
var previousDate;

ctrl.$$parserName = type;
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
if (regexp.test(value)) {
Expand All @@ -1447,6 +1446,7 @@ function createDateInputType(type, regexp, parseDate, format) {
}
return parsedDate;
}
ctrl.$$parserName = type;
return undefined;
});

Expand Down Expand Up @@ -1499,22 +1499,28 @@ function createDateInputType(type, regexp, parseDate, format) {
};
}

function badInputChecker(scope, element, attr, ctrl) {
function badInputChecker(scope, element, attr, ctrl, parserName) {
var node = element[0];
var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
if (nativeValidation) {
ctrl.$parsers.push(function(value) {
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
return validity.badInput || validity.typeMismatch ? undefined : value;
if (validity.badInput || validity.typeMismatch) {
ctrl.$$parserName = parserName;
return undefined;
}

return value;
});
}
}

function numberFormatterParser(ctrl) {
ctrl.$$parserName = 'number';
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
if (NUMBER_REGEXP.test(value)) return parseFloat(value);

ctrl.$$parserName = 'number';
return undefined;
});

Expand Down Expand Up @@ -1596,7 +1602,7 @@ function isValidForStep(viewValue, stepBase, step) {
}

function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
badInputChecker(scope, element, attr, ctrl);
badInputChecker(scope, element, attr, ctrl, 'number');
numberFormatterParser(ctrl);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);

Expand Down Expand Up @@ -1643,7 +1649,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}

function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
badInputChecker(scope, element, attr, ctrl);
badInputChecker(scope, element, attr, ctrl, 'range');
numberFormatterParser(ctrl);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);

Expand Down Expand Up @@ -1782,7 +1788,6 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
stringBasedInputType(ctrl);

ctrl.$$parserName = 'url';
ctrl.$validators.url = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
Expand All @@ -1795,7 +1800,6 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
stringBasedInputType(ctrl);

ctrl.$$parserName = 'email';
ctrl.$validators.email = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
Expand Down
9 changes: 8 additions & 1 deletion src/ng/directive/ngModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
this.$$ngModelSet = this.$$parsedNgModelAssign;
this.$$pendingDebounce = null;
this.$$parserValid = undefined;
this.$$parserName = 'parse';

this.$$currentValidationRunId = 0;

Expand Down Expand Up @@ -607,7 +608,8 @@ NgModelController.prototype = {
processAsyncValidators();

function processParseErrors() {
var errorKey = that.$$parserName || 'parse';
var errorKey = that.$$parserName;

if (isUndefined(that.$$parserValid)) {
setValidity(errorKey, null);
} else {
Expand All @@ -619,6 +621,7 @@ NgModelController.prototype = {
setValidity(name, null);
});
}

// Set the parse error last, to prevent unsetting it, should a $validators key == parserName
setValidity(errorKey, that.$$parserValid);
return that.$$parserValid;
Expand Down Expand Up @@ -721,6 +724,10 @@ NgModelController.prototype = {

this.$$parserValid = isUndefined(modelValue) ? undefined : true;

// Reset any previous parse error
this.$setValidity(this.$$parserName, null);
this.$$parserName = 'parse';

if (this.$$parserValid) {
for (var i = 0; i < this.$parsers.length; i++) {
modelValue = this.$parsers[i](modelValue);
Expand Down
98 changes: 98 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,37 @@ describe('input', function() {
helper.changeInputValueTo('stuff');
expect(inputElm.val()).toBe('stuff');
expect($rootScope.value).toBeUndefined();
expect(inputElm).toHaveClass('ng-invalid-month');
expect(inputElm).toBeInvalid();
});


it('should not set error=month when a later parser returns undefined', function() {
var inputElm = helper.compileInput('<input type="month" ng-model="value"/>');
var ctrl = inputElm.controller('ngModel');

ctrl.$parsers.push(function() {
return undefined;
});

inputElm[0].setAttribute('type', 'text');

helper.changeInputValueTo('2017-01');

expect($rootScope.value).toBeUndefined();
expect(ctrl.$error.month).toBeFalsy();
expect(ctrl.$error.parse).toBeTruthy();
expect(inputElm).not.toHaveClass('ng-invalid-month');
expect(inputElm).toHaveClass('ng-invalid-parse');
expect(inputElm).toBeInvalid();

helper.changeInputValueTo('asdf');

expect($rootScope.value).toBeUndefined();
expect(ctrl.$error.month).toBeTruthy();
expect(ctrl.$error.parse).toBeFalsy();
expect(inputElm).toHaveClass('ng-invalid-month');
expect(inputElm).not.toHaveClass('ng-invalid-parse');
expect(inputElm).toBeInvalid();
});

Expand Down Expand Up @@ -2457,6 +2488,73 @@ describe('input', function() {
expect($rootScope.value).toBe(123214124123412412e-26);
});

it('should not set $error number if any other parser fails', function() {
var inputElm = helper.compileInput('<input type="number" ng-model="age"/>');
var ctrl = inputElm.controller('ngModel');

var previousParserFail = false;
var laterParserFail = false;

ctrl.$parsers.unshift(function(value) {
return previousParserFail ? undefined : value;
});

ctrl.$parsers.push(function(value) {
return laterParserFail ? undefined : value;
});

// to allow non-number values, we have to change type so that
// the browser which have number validation will not interfere with
// this test.
inputElm[0].setAttribute('type', 'text');

helper.changeInputValueTo('123X');
expect(inputElm.val()).toBe('123X');

expect($rootScope.age).toBeUndefined();
expect(inputElm).toBeInvalid();
expect(ctrl.$error.number).toBe(true);
expect(ctrl.$error.parse).toBeFalsy();
expect(inputElm).toHaveClass('ng-invalid-number');
expect(inputElm).not.toHaveClass('ng-invalid-parse');

previousParserFail = true;
helper.changeInputValueTo('123');
expect(inputElm.val()).toBe('123');

expect($rootScope.age).toBeUndefined();
expect(inputElm).toBeInvalid();
expect(ctrl.$error.number).toBeFalsy();
expect(ctrl.$error.parse).toBe(true);
expect(inputElm).not.toHaveClass('ng-invalid-number');
expect(inputElm).toHaveClass('ng-invalid-parse');

previousParserFail = false;
laterParserFail = true;

helper.changeInputValueTo('1234');
expect(inputElm.val()).toBe('1234');

expect($rootScope.age).toBeUndefined();
expect(inputElm).toBeInvalid();
expect(ctrl.$error.number).toBeFalsy();
expect(ctrl.$error.parse).toBe(true);
expect(inputElm).not.toHaveClass('ng-invalid-number');
expect(inputElm).toHaveClass('ng-invalid-parse');

laterParserFail = false;

helper.changeInputValueTo('12345');
expect(inputElm.val()).toBe('12345');

expect($rootScope.age).toBe(12345);
expect(inputElm).toBeValid();
expect(ctrl.$error.number).toBeFalsy();
expect(ctrl.$error.parse).toBeFalsy();
expect(inputElm).not.toHaveClass('ng-invalid-number');
expect(inputElm).not.toHaveClass('ng-invalid-parse');
});


describe('min', function() {

Expand Down
2 changes: 1 addition & 1 deletion test/ng/directive/ngModelSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1378,13 +1378,13 @@ describe('ngModel', function() {
}
};

ctrl.$$parserName = 'parserOrValidator';
ctrl.$parsers.push(function(value) {
switch (value) {
case 'allInvalid':
case 'stillAllInvalid':
case 'parseInvalid-validatorsValid':
case 'stillParseInvalid-validatorsValid':
ctrl.$$parserName = 'parserOrValidator';
return undefined;
default:
return value;
Expand Down