Skip to content

Commit be17bcb

Browse files
committed
fix(input): re-validate when partially editing date-family inputs
Fixes angular#12207
1 parent 3d158f6 commit be17bcb

File tree

2 files changed

+105
-2
lines changed

2 files changed

+105
-2
lines changed

src/ng/directive/input.js

+28-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
3232
var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
3333
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
3434

35+
var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown';
36+
var PARTIAL_VALIDATION_TYPES = createMap();
37+
forEach('date,datetime-local,month,time,week'.split(','), function(type) {
38+
PARTIAL_VALIDATION_TYPES[type] = true;
39+
});
40+
3541
var inputType = {
3642

3743
/**
@@ -1118,6 +1124,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
11181124
});
11191125
}
11201126

1127+
var timeout;
1128+
11211129
var listener = function(ev) {
11221130
if (timeout) {
11231131
$browser.defer.cancel(timeout);
@@ -1147,8 +1155,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
11471155
if ($sniffer.hasEvent('input')) {
11481156
element.on('input', listener);
11491157
} else {
1150-
var timeout;
1151-
11521158
var deferListener = function(ev, input, origValue) {
11531159
if (!timeout) {
11541160
timeout = $browser.defer(function() {
@@ -1180,6 +1186,26 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
11801186
// or form autocomplete on newer browser, we need "change" event to catch it
11811187
element.on('change', listener);
11821188

1189+
// Some native input types (date-family) have the ability to change validity without
1190+
// firing any input/change events.
1191+
// For these event types, when native validators are present and the browser supports the type,
1192+
// check for validity changes on various DOM events.
1193+
if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) {
1194+
element.on(PARTIAL_VALIDATION_EVENTS, function(ev) {
1195+
if (!timeout) {
1196+
var validity = this[VALIDITY_STATE_PROPERTY] || {};
1197+
var origBadInput = validity.badInput;
1198+
var origTypeMismatch = validity.typeMismatch;
1199+
timeout = $browser.defer(function() {
1200+
timeout = null;
1201+
if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) {
1202+
listener(ev);
1203+
}
1204+
});
1205+
}
1206+
});
1207+
}
1208+
11831209
ctrl.$render = function() {
11841210
// Workaround for Firefox validation #12102.
11851211
var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue;

test/ng/directive/inputSpec.js

+77
Original file line numberDiff line numberDiff line change
@@ -1918,6 +1918,83 @@ describe('input', function() {
19181918
});
19191919
});
19201920

1921+
['month', 'week', 'time', 'date', 'datetime-local'].forEach(function(inputType) {
1922+
if (jqLite('<input type="' + inputType + '">').attr('type') !== inputType) {
1923+
return;
1924+
}
1925+
1926+
describe(inputType, function() {
1927+
they('should re-validate and dirty when partially editing the input value ($prop event)',
1928+
['keydown', 'wheel', 'mousedown'],
1929+
function(validationEvent) {
1930+
var mockValidity = {valid: true, badInput: false};
1931+
var inputElm = helper.compileInput('<input type="' + inputType + '" ng-model="val" name="alias" />', mockValidity);
1932+
1933+
expect(inputElm).toBeValid();
1934+
expect($rootScope.form.alias.$pristine).toBeTruthy();
1935+
1936+
inputElm.triggerHandler({type: validationEvent});
1937+
mockValidity.valid = false;
1938+
mockValidity.badInput = true;
1939+
$browser.defer.flush();
1940+
expect(inputElm).toBeInvalid();
1941+
expect($rootScope.form.alias.$pristine).toBeFalsy();
1942+
}
1943+
);
1944+
1945+
they('should do nothing when $prop event fired but validity does not change',
1946+
['keydown', 'wheel', 'mousedown'],
1947+
function(validationEvent) {
1948+
var mockValidity = {valid: true, badInput: false};
1949+
var inputElm = helper.compileInput('<input type="' + inputType + '" ng-model="val" name="alias" />', mockValidity);
1950+
1951+
expect(inputElm).toBeValid();
1952+
expect($rootScope.form.alias.$pristine).toBeTruthy();
1953+
1954+
inputElm.triggerHandler({type: validationEvent});
1955+
$browser.defer.flush();
1956+
expect(inputElm).toBeValid();
1957+
expect($rootScope.form.alias.$pristine).toBeTruthy();
1958+
}
1959+
);
1960+
1961+
they('should re-validate dirty when already $invalid and partially editing the input value ($prop event)',
1962+
['keydown', 'wheel', 'mousedown'],
1963+
function(validationEvent) {
1964+
var mockValidity = {valid: false, valueMissing: true, badInput: false};
1965+
var inputElm = helper.compileInput('<input type="' + inputType + '" required ng-model="val" name="alias" />', mockValidity);
1966+
1967+
expect(inputElm).toBeInvalid();
1968+
expect($rootScope.form.alias.$pristine).toBeTruthy();
1969+
1970+
inputElm.triggerHandler({type: validationEvent});
1971+
mockValidity.valid = false;
1972+
mockValidity.valueMissing = true;
1973+
mockValidity.badInput = true;
1974+
$browser.defer.flush();
1975+
expect(inputElm).toBeInvalid();
1976+
expect($rootScope.form.alias.$pristine).toBeFalsy();
1977+
}
1978+
);
1979+
1980+
they('should do nothing when already $invalid and $prop event fired but validity does not change',
1981+
['keydown', 'wheel', 'mousedown'],
1982+
function(validationEvent) {
1983+
var mockValidity = {valid: false, valueMissing: true, badInput: false};
1984+
var inputElm = helper.compileInput('<input type="' + inputType + '" required ng-model="val" name="alias" />', mockValidity);
1985+
1986+
expect(inputElm).toBeInvalid();
1987+
expect($rootScope.form.alias.$pristine).toBeTruthy();
1988+
1989+
inputElm.triggerHandler({type: validationEvent});
1990+
$browser.defer.flush();
1991+
expect(inputElm).toBeInvalid();
1992+
expect($rootScope.form.alias.$pristine).toBeTruthy();
1993+
}
1994+
);
1995+
});
1996+
});
1997+
19211998

19221999
describe('number', function() {
19232000

0 commit comments

Comments
 (0)