From ab031d66d3255f4920a6a3eb22e64d20e7b5035b Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sat, 23 Jan 2016 13:57:22 -0800 Subject: [PATCH] fix(input): re-validate when partially editing date-family inputs Fixes #12207 --- src/ng/directive/input.js | 30 ++++++++++++- test/ng/directive/inputSpec.js | 77 ++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 27098a168560..4d60afb6e527 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -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 = { /** @@ -1118,6 +1124,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } + var timeout; + var listener = function(ev) { if (timeout) { $browser.defer.cancel(timeout); @@ -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() { @@ -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; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 4b9727913737..9dc40d93e09d 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1943,6 +1943,83 @@ describe('input', function() { }); }); + ['month', 'week', 'time', 'date', 'datetime-local'].forEach(function(inputType) { + if (jqLite('').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('', 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('', 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('', 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('', 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() {