-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix(input): improve html5 validation support #7937
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -862,15 +862,29 @@ function validate(ctrl, validatorName, validity, value){ | |
return validity ? value : undefined; | ||
} | ||
|
||
function testFlags(validity, flags) { | ||
var i, flag; | ||
if (flags) { | ||
for (i=0; i<flags.length; ++i) { | ||
flag = flags[i]; | ||
if (validity[flag]) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
function addNativeHtml5Validators(ctrl, validatorName, element) { | ||
var validity = element.prop('validity'); | ||
// Pass validity so that behaviour can be mocked easier. | ||
function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) { | ||
if (isObject(validity)) { | ||
ctrl.$$hasNativeValidators = true; | ||
var validator = function(value) { | ||
// Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can | ||
// perform the required validation) | ||
if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError || | ||
validity.typeMismatch) && !validity.valueMissing) { | ||
if (!ctrl.$error[validatorName] && | ||
!testFlags(validity, ignoreFlags) && | ||
testFlags(validity, badFlags)) { | ||
ctrl.$setValidity(validatorName, false); | ||
return; | ||
} | ||
|
@@ -881,8 +895,9 @@ function addNativeHtml5Validators(ctrl, validatorName, element) { | |
} | ||
|
||
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
var validity = element.prop('validity'); | ||
var validity = element.prop(VALIDITY_STATE_PROPERTY); | ||
var placeholder = element[0].placeholder, noevent = {}; | ||
ctrl.$$validityState = validity; | ||
|
||
// In composition mode, users are still inputing intermediate text buffer, | ||
// hold the listener until composition is done. | ||
|
@@ -921,16 +936,16 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { | |
value = trim(value); | ||
} | ||
|
||
if (ctrl.$viewValue !== value || | ||
// If the value is still empty/falsy, and there is no `required` error, run validators | ||
// again. This enables HTML5 constraint validation errors to affect Angular validation | ||
// even when the first character entered causes an error. | ||
(validity && value === '' && !validity.valueMissing)) { | ||
// If a control is suffering from bad input, browsers discard its value, so it may be | ||
// necessary to revalidate even if the control's value is the same empty value twice in | ||
// a row. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added some comments |
||
var revalidate = validity && ctrl.$$hasNativeValidators; | ||
if (ctrl.$viewValue !== value || (value === '' && revalidate)) { | ||
if (scope.$$phase) { | ||
ctrl.$setViewValue(value, event); | ||
ctrl.$setViewValue(value, event, revalidate); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems a shame that we must touch so many function on NgModelController with this |
||
} else { | ||
scope.$apply(function() { | ||
ctrl.$setViewValue(value, event); | ||
ctrl.$setViewValue(value, event, revalidate); | ||
}); | ||
} | ||
} | ||
|
@@ -1079,6 +1094,8 @@ function createDateInputType(type, regexp, parseDate, format) { | |
}; | ||
} | ||
|
||
var numberBadFlags = ['badInput']; | ||
|
||
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
textInputType(scope, element, attr, ctrl, $sniffer, $browser); | ||
|
||
|
@@ -1093,7 +1110,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { | |
} | ||
}); | ||
|
||
addNativeHtml5Validators(ctrl, 'number', element); | ||
addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's needed for other types, eventually we will want to do this for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This is because of the really, really stupid constraint validation API, it makes zero sense, but it's what we've got =() There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But if we insist that these parameters are always arrays then we don't need the check right? Is there anywhere that a null is going to be passed where we can't insist that it is an empty array instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The question is whether you prefer testing if flags is null, or if you prefer creating an extra global object --- my preference is on the former here |
||
|
||
ctrl.$formatters.push(function(value) { | ||
return ctrl.$isEmpty(value) ? '' : '' + value; | ||
|
@@ -1777,11 +1794,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ | |
* event defined in `ng-model-options`. this method is rarely needed as `NgModelController` | ||
* usually handles calling this in response to input events. | ||
*/ | ||
this.$commitViewValue = function() { | ||
this.$commitViewValue = function(revalidate) { | ||
var viewValue = ctrl.$viewValue; | ||
|
||
$timeout.cancel(pendingDebounce); | ||
if (ctrl.$$lastCommittedViewValue === viewValue) { | ||
if (!revalidate && ctrl.$$lastCommittedViewValue === viewValue) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CC @petebacondarwin / @shahata --- It's possible for the old value and new value to be the same here, because HTML5 will report controlls suffering from bad input's value as the empty string --- but we should revalidate anyways just so the native HTML validators do their business. However, it may not be desirable to update the "last committed value" in that case, what do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does it mean for the native validators to do their business? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We occasionally need to revalidate, even if the actual value hasn't changed. But I guess by that point, the last committed value is probably the empty string anyways. So maybe it's not worth worrying about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably have some unit tests that demonstrate the various scenarios - at least for posterity. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not every time, only when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, the ngModelOptions stuff added a ton of complexity to this machinery that didn't exist previously, and there were no tests against it breaking the html5 validation stuff because it's not really possible to test without without the hacks here (those hacks probably should have existed initially, but hey). So what we need to do is figure out a way for them to work well together in tandum. Computing a diff between ValidityState objects is problematic, because A) ValidityState keys are not always enumerable (Firefox) which makes a diffing algorithm difficult and makes us have to keep track of possible ValidityState flags which are not necessarily never changing, maintenance hell, and B) it's just a stupid amount of work that isn't really needed anywhere. You're right that this is broken in the case of invalid -> empty, so that's fixable, but the gunk with ngModelOptions is a lot harder because that piece of machinery is overly complex and not built with constraint validation in mind, which is why you guys are CC'd here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree diffing the ValidityState is not pretty/cheap and I really don't like it too, I'm just trying to think of a way to do this that will work in all cases. What about the option to run all validators anyway in case the string is empty? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with that, at least in the case where we're using html5 validators There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool! |
||
return; | ||
} | ||
ctrl.$$lastCommittedViewValue = viewValue; | ||
|
@@ -1846,14 +1863,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ | |
* @param {string} value Value from the view. | ||
* @param {string} trigger Event that triggered the update. | ||
*/ | ||
this.$setViewValue = function(value, trigger) { | ||
this.$setViewValue = function(value, trigger, revalidate) { | ||
ctrl.$viewValue = value; | ||
if (!ctrl.$options || ctrl.$options.updateOnDefault) { | ||
ctrl.$$debounceViewValueCommit(trigger); | ||
ctrl.$$debounceViewValueCommit(trigger, revalidate); | ||
} | ||
}; | ||
|
||
this.$$debounceViewValueCommit = function(trigger) { | ||
this.$$debounceViewValueCommit = function(trigger, revalidate) { | ||
var debounceDelay = 0, | ||
options = ctrl.$options, | ||
debounce; | ||
|
@@ -1872,10 +1889,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ | |
$timeout.cancel(pendingDebounce); | ||
if (debounceDelay) { | ||
pendingDebounce = $timeout(function() { | ||
ctrl.$commitViewValue(); | ||
ctrl.$commitViewValue(revalidate); | ||
}, debounceDelay); | ||
} else { | ||
ctrl.$commitViewValue(); | ||
ctrl.$commitViewValue(revalidate); | ||
} | ||
}; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this so that the input source doesn't have to say
element.prop('ngMockValidity') || element.prop('validity')
It can probably be done different ways too