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

Commit

Permalink
fix(input): re-validate when partially editing date-family inputs
Browse files Browse the repository at this point in the history
Fixes #12207
Closes #13886
  • Loading branch information
jbedard authored and gkalpak committed Feb 18, 2016
1 parent 2a7d4c1 commit e383804
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 2 deletions.
30 changes: 28 additions & 2 deletions src/ng/directive/input.js
Expand Up @@ -32,6 +32,12 @@ var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/;
var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/;
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;

var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown';
var PARTIAL_VALIDATION_TYPES = createMap();
forEach('date,datetime-local,month,time,week'.split(','), function(type) {
PARTIAL_VALIDATION_TYPES[type] = true;
});

var inputType = {

/**
Expand Down Expand Up @@ -1118,6 +1124,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
});
}

var timeout;

var listener = function(ev) {
if (timeout) {
$browser.defer.cancel(timeout);
Expand Down Expand Up @@ -1147,8 +1155,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
if ($sniffer.hasEvent('input')) {
element.on('input', listener);
} else {
var timeout;

var deferListener = function(ev, input, origValue) {
if (!timeout) {
timeout = $browser.defer(function() {
Expand Down Expand Up @@ -1180,6 +1186,26 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// or form autocomplete on newer browser, we need "change" event to catch it
element.on('change', listener);

// Some native input types (date-family) have the ability to change validity without
// firing any input/change events.
// For these event types, when native validators are present and the browser supports the type,
// check for validity changes on various DOM events.
if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) {
element.on(PARTIAL_VALIDATION_EVENTS, function(ev) {
if (!timeout) {
var validity = this[VALIDITY_STATE_PROPERTY];
var origBadInput = validity.badInput;
var origTypeMismatch = validity.typeMismatch;
timeout = $browser.defer(function() {
timeout = null;
if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) {
listener(ev);
}
});
}
});
}

ctrl.$render = function() {
// Workaround for Firefox validation #12102.
var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue;
Expand Down
77 changes: 77 additions & 0 deletions test/ng/directive/inputSpec.js
Expand Up @@ -2103,6 +2103,83 @@ describe('input', function() {
});
});

['month', 'week', 'time', 'date', 'datetime-local'].forEach(function(inputType) {
if (jqLite('<input type="' + inputType + '">').prop('type') !== inputType) {
return;
}

describe(inputType, function() {
they('should re-validate and dirty when partially editing the input value ($prop event)',
['keydown', 'wheel', 'mousedown'],
function(validationEvent) {
var mockValidity = {valid: true, badInput: false};
var inputElm = helper.compileInput('<input type="' + inputType + '" ng-model="val" name="alias" />', mockValidity);

expect(inputElm).toBeValid();
expect($rootScope.form.alias.$pristine).toBeTruthy();

inputElm.triggerHandler({type: validationEvent});
mockValidity.valid = false;
mockValidity.badInput = true;
$browser.defer.flush();
expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$pristine).toBeFalsy();
}
);

they('should do nothing when $prop event fired but validity does not change',
['keydown', 'wheel', 'mousedown'],
function(validationEvent) {
var mockValidity = {valid: true, badInput: false};
var inputElm = helper.compileInput('<input type="' + inputType + '" ng-model="val" name="alias" />', mockValidity);

expect(inputElm).toBeValid();
expect($rootScope.form.alias.$pristine).toBeTruthy();

inputElm.triggerHandler({type: validationEvent});
$browser.defer.flush();
expect(inputElm).toBeValid();
expect($rootScope.form.alias.$pristine).toBeTruthy();
}
);

they('should re-validate dirty when already $invalid and partially editing the input value ($prop event)',
['keydown', 'wheel', 'mousedown'],
function(validationEvent) {
var mockValidity = {valid: false, valueMissing: true, badInput: false};
var inputElm = helper.compileInput('<input type="' + inputType + '" required ng-model="val" name="alias" />', mockValidity);

expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$pristine).toBeTruthy();

inputElm.triggerHandler({type: validationEvent});
mockValidity.valid = false;
mockValidity.valueMissing = true;
mockValidity.badInput = true;
$browser.defer.flush();
expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$pristine).toBeFalsy();
}
);

they('should do nothing when already $invalid and $prop event fired but validity does not change',
['keydown', 'wheel', 'mousedown'],
function(validationEvent) {
var mockValidity = {valid: false, valueMissing: true, badInput: false};
var inputElm = helper.compileInput('<input type="' + inputType + '" required ng-model="val" name="alias" />', mockValidity);

expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$pristine).toBeTruthy();

inputElm.triggerHandler({type: validationEvent});
$browser.defer.flush();
expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$pristine).toBeTruthy();
}
);
});
});


describe('number', function() {

Expand Down

0 comments on commit e383804

Please sign in to comment.