在用户离开字段后验证字段

使用 AngularJS,我可以使用 ng-pristineng-dirty来检测用户是否已经进入字段。但是,我希望只在用户离开字段区域之后才进行客户端验证。这是因为当用户输入电子邮件或电话等字段时,总是会抛出一个错误,直到他们完成输入完整的电子邮件,这不是最佳的用户体验。

例子


更新:

Angular 现在提供一个自定义模糊事件: Https://docs.angularjs.org/api/ng/directive/ngblur

137459 次浏览

It might work for you to write a custom directive that wraps the javascript blur() method (and runs a validation function when triggered); there's an Angular issue that has a sample one (as well as a generic directive that can bind to other events not natively supported by Angular):

https://github.com/angular/angular.js/issues/1277

If you don't want to go that route, your other option would be to set up $watch on the field, again triggering validation when the field is filled out.

I solved this by expanding on what @jlmcdonald suggested. I created a directive that would automatically be applied to all input and select elements:

var blurFocusDirective = function () {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, elm, attr, ctrl) {
if (!ctrl) {
return;
}


elm.on('focus', function () {
elm.addClass('has-focus');


scope.$apply(function () {
ctrl.hasFocus = true;
});
});


elm.on('blur', function () {
elm.removeClass('has-focus');
elm.addClass('has-visited');


scope.$apply(function () {
ctrl.hasFocus = false;
ctrl.hasVisited = true;
});
});


elm.closest('form').on('submit', function () {
elm.addClass('has-visited');


scope.$apply(function () {
ctrl.hasFocus = false;
ctrl.hasVisited = true;
});
});


}
};
};


app.directive('input', blurFocusDirective);
app.directive('select', blurFocusDirective);

This will add has-focus and has-visited classes to various elements as the user focuses/visits the elements. You can then add these classes to your CSS rules to show validation errors:

input.has-visited.ng-invalid:not(.has-focus) {
background-color: #ffeeee;
}

This works well in that elements still get $invalid properties etc, but the CSS can be used to give the user a better experience.

To pick up further on the given answers, you can simplify input tagging by using CSS3 pseudo-classes and only marking visited fields with a class to delay displaying validation errors until the user lost focus on the field:

(Example requires jQuery)

JavaScript

module = angular.module('app.directives', []);
module.directive('lateValidateForm', function () {
return {
restrict: 'AC',
link: function (scope, element, attrs) {
$inputs = element.find('input, select, textarea');


$inputs.on('blur', function () {
$(this).addClass('has-visited');
});


element.on('submit', function () {
$inputs.addClass('has-visited');
});
}
};
});

CSS

input.has-visited:not(:focus):required:invalid,
textarea.has-visited:not(:focus):required:invalid,
select.has-visited:not(:focus):required:invalid {
color: #b94a48;
border-color: #ee5f5b;
}

HTML

<form late-validate-form name="userForm">
<input type="email" name="email" required />
</form>

Regarding @lambinator's solution... I was getting the following error in angular.js 1.2.4:

Error: [$rootScope:inprog] $digest already in progress

I'm not sure if I did something wrong or if this is a change in Angular, but removing the scope.$apply statements resolved the problem and the classes/states are still getting updated.

If you are also seeing this error, give the following a try:

var blurFocusDirective = function () {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, elm, attr, ctrl) {
if (!ctrl) {
return;
}
elm.on('focus', function () {
elm.addClass('has-focus');
ctrl.$hasFocus = true;
});


elm.on('blur', function () {
elm.removeClass('has-focus');
elm.addClass('has-visited');
ctrl.$hasFocus = false;
ctrl.$hasVisited = true;
});


elm.closest('form').on('submit', function () {
elm.addClass('has-visited');


scope.$apply(function () {
ctrl.hasFocus = false;
ctrl.hasVisited = true;
});
});
}
};
};
app.directive('input', blurFocusDirective);
app.directive('select', blurFocusDirective);

I managed to do this with a pretty simple bit of CSS. This does require that the error messages be siblings of the input they relate to, and that they have a class of error.

:focus ~ .error {
display:none;
}

After meeting those two requirements, this will hide any error message related to a focused input field, something that I think angularjs should be doing anyway. Seems like an oversight.

based on @nicolas answer.. Pure CSS should the trick, it will only show the error message on blur

<input type="email" id="input-email" required
placeholder="Email address" class="form-control" name="email"
ng-model="userData.email">
<p ng-show="form.email.$error.email" class="bg-danger">This is not a valid email.</p>

CSS

.ng-invalid:focus ~ .bg-danger {
display:none;
}

You can dynamically set the has-error css class (assuming you're using bootstrap) using ng-class and a property on the scope of the associated controller:

plunkr: http://plnkr.co/edit/HYDlaTNThZE02VqXrUCH?p=info

HTML:

<div ng-class="{'has-error': badEmailAddress}">
<input type="email" class="form-control" id="email" name="email"
ng-model="email"
ng-blur="emailBlurred(email.$valid)">
</div>

Controller:

$scope.badEmailAddress = false;


$scope.emailBlurred = function (isValid) {
$scope.badEmailAddress = !isValid;
};

Angular 1.3 now has ng-model-options, and you can set the option to { 'updateOn': 'blur'} for example, and you can even debounce, when the use is either typing too fast, or you want to save a few expensive DOM operations (like a model writing to multiple DOM places and you don't want a $digest cycle happening on every key down)

https://docs.angularjs.org/guide/forms#custom-triggers and https://docs.angularjs.org/guide/forms#non-immediate-debounced-model-updates

By default, any change to the content will trigger a model update and form validation. You can override this behavior using the ngModelOptions directive to bind only to specified list of events. I.e. ng-model-options="{ updateOn: 'blur' }" will update and validate only after the control loses focus. You can set several events using a space delimited list. I.e. ng-model-options="{ updateOn: 'mousedown blur' }"

And debounce

You can delay the model update/validation by using the debounce key with the ngModelOptions directive. This delay will also apply to parsers, validators and model flags like $dirty or $pristine.

I.e. ng-model-options="{ debounce: 500 }" will wait for half a second since the last content change before triggering the model update and form validation.

ng-model-options in AngularJS 1.3 (beta as of this writing) is documented to support {updateOn: 'blur'}. For earlier versions, something like the following worked for me:

myApp.directive('myForm', function() {
return {
require: 'form',
link: function(scope, element, attrs, formController) {
scope.validate = function(name) {
formController[name].isInvalid
= formController[name].$invalid;
};
}
};
});

With a template like this:

<form name="myForm" novalidate="novalidate" data-my-form="">
<input type="email" name="eMail" required="required" ng-blur="validate('eMail')" />
<span ng-show="myForm.eMail.isInvalid">Please enter a valid e-mail address.</span>
<button type="submit">Submit Form</button>
</form>

If you use bootstrap 3 and lesscss you can enable on blur validation with the following less snippet:

:focus ~ .form-control-feedback.glyphicon-ok {
display:none;
}


:focus ~ .form-control-feedback.glyphicon-remove {
display:none;
}


.has-feedback > :focus {
& {
.form-control-focus();
}
}

outI used a directive. Here is the code:

app.directive('onBlurVal', function () {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {


element.on('focus', function () {
element.next().removeClass('has-visited');
element.next().addClass('has-focus');
});


element.on('blur', function () {


element.next().removeClass('has-focus');
element.next().addClass('has-visited');
});
}
}
})

All my input control has a span element as the next element, which is where my validation message is displayed and so the directive as an attribute is added to each input control.

I also have (optional).has-focus and has-visited css class in my css file which you see being referenced in the directive.

NOTE: remember to add 'on-blur-val' exactly this way to your input control without the apostrophes

Here is an example using ng-messages (available in angular 1.3) and a custom directive.

Validation message is displayed on blur for the first time user leaves the input field, but when he corrects the value, validation message is removed immediately (not on blur anymore).

JavaScript

myApp.directive("validateOnBlur", [function() {
var ddo = {
restrict: "A",
require: "ngModel",
scope: {},
link: function(scope, element, attrs, modelCtrl) {
element.on('blur', function () {
modelCtrl.$showValidationMessage = modelCtrl.$dirty;
scope.$apply();
});
}
};
return ddo;
}]);

HTML

<form name="person">
<input type="text" ng-model="item.firstName" name="firstName"
ng-minlength="3" ng-maxlength="20" validate-on-blur required />
<div ng-show="person.firstName.$showValidationMessage" ng-messages="person.firstName.$error">
<span ng-message="required">name is required</span>
<span ng-message="minlength">name is too short</span>
<span ng-message="maxlength">name is too long</span>
</div>
</form>

PS. Don't forget to download and include ngMessages in your module:

var myApp = angular.module('myApp', ['ngMessages']);

By using ng-focus you can achieve your goal. you need to provide ng-focus in your input field. And while writing your ng-show derivatives you have to write a logic not equal too. Like the below code:

<input type="text" class="form-control" name="inputPhone" ng-model="demo.phoneNumber" required ng-focus> <div ng-show="demoForm.inputPhone.$dirty && demoForm.inputPhone.$invalid && !demoForm.inputPhone.$focused"></div>

This seems to be implemented as standard in newer versions of angular.

The classes ng-untouched and ng-touched are set respectively before and after the user has had focus on an validated element.

CSS

input.ng-touched.ng-invalid {
border-color: red;
}

From version 1.3.0 this can easily be done with $touched, which is true after the user has left the field.

<p ng-show="form.field.$touched && form.field.$invalid">Error</p>

https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

We can use onfocus and onblur functions. Would be simple and best.

<body ng-app="formExample">
<div ng-controller="ExampleController">
<form novalidate class="css-form">
Name: <input type="text" ng-model="user.name" ng-focus="onFocusName='focusOn'" ng-blur="onFocusName=''" ng-class="onFocusName" required /><br />
E-mail: <input type="email" ng-model="user.email" ng-focus="onFocusEmail='focusOn'" ng-blur="onFocusEmail=''" ng-class="onFocusEmail" required /><br />
</form>
</div>


<style type="text/css">
.css-form input.ng-invalid.ng-touched {
border: 1px solid #FF0000;
background:#FF0000;
}
.css-form input.focusOn.ng-invalid {
border: 1px solid #000000;
background:#FFFFFF;
}
</style>

Try here:

http://plnkr.co/edit/NKCmyru3knQiShFZ96tp?p=preview

Use field state $touched The field has been touched for this as shown in below example.

<div ng-show="formName.firstName.$touched && formName.firstName.$error.required">
You must enter a value
</div>