I have a simple directive called po-datepicker, it displays a datepicker on the screen, but allows the user to type a date manually:
<input type="text" ng-model="model" po-datepicker required />
and this is the directive:
myApp.directive('poDatepicker', function () {
return {
require: ['?^ngModel'],
restrict: 'A',
link: function ($scope, elem, attrs, ctrl) {
var ngModel = ctrl[0];
var picker = elem.datepicker();
picker.on('changeDate', function(e) {
ngModel.$setViewValue(e.date);
...
});
elem.parent().find('button').on('click', function() {
picker.datepicker('show');
});
var changeFn = function(e) {
// Here I have some logic that calls $setViewValue();
};
picker.on('hide', changeFn);
elem.on('keyup blur', changeFn);
}
};
});
this works as expected, but when I try to type a value in the input, it updates the ngModel, changing the variable in the scope, how can I prevent ngModel from being changed in the input?
Here is a plunkr, try manually writing a value and you'll understand what I'm talking.
Actually, after some research, I found a solution for this problem.
What I found on forums and questions is that I needed to unbind the element's events, like this:
elem.unbind('input').unbind('keydown').unbind('change');
But that solution didn't work as expected.
The problem is that I'm currently using Angular 1.2.x, I found out that you need also to set some priority to the directive, such as:
return {
require: ['?^ngModel'],
priority: 1,
...
}
The priority: 1 is needed in this case, because of the priority of some internal Angular.js directives.
Here is an updated plunker with the right priority set up.
Just add 'disabled' to the input http://plnkr.co/edit/xFeAmSCtKdNSQR1zbAsd?p=preview
<input type="text" class="form-control" ng-model="test" po-datepicker required feedback disabled/>
I am using angular-strap datepicker from http://mgcrea.github.io/angular-strap/##datepickers for my angularjs SPA project. when editing an existing record using ngModel two way binding, Angular doesn't detect the date field change and even if I force the form to save by updating another non date field, the new date is not updated in backend. Here is the related part in my html file:
<input name="DecisionDatePicker" id="ddpID" type="text" class="form-control input-medium" tabindex="14" placeholder="{{vm.format}}"
data-date-format="MM-dd-yyyy"
data-ng-model="vm.formData.dateDecision"
data-ng-required="vm.decisionDateRequired"
bs-datepicker />
I do not do anything special in my js file. I am using breezejs for my data handling and it is working fine for other fields.
what am I doing wrong? Any help is appreciated.
I've had this same problem and I solved with a custom directive that updates the model when the bs-datepicker dispatches a blur event:
angular.module('moduleName').directive('updateModelOnBlur',
['$parse', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elm, attrs) {
elm.bind("blur", function (event) {
scope.$apply(function () {
var model = $parse(attrs.ngModel);
model.assign( scope, elm.context.value );
});
});
}
};
}]
);
I know this is just a hack, and not a final solution for the problem. But, if you are stuck and want to keep going, sometimes a hack might be useful.
You can also include $filter service in the directive if you want to manipulate date format before updating model. ;)
I'm trying to write my own set of directives. I have written following two directives:
eaValidateEmail (Validates the format of the email address)
eaValidateUnique (Will validate the uniqueness by a call to a rest service once complete)
What I want to achieve:
First the eaValidateEmail directive is executed which returns false until the format of the email is correct
Then and only then the eaValidateUnique directive should execute and check if the email address is taken already over a rest service. If the value is not found it will return true, else it will return false.
What's happening
When I only add the eaValidateEmail directive, everything is working and the format of the email is validated.
But as soon I add the eaValidateUnique directive then the eaValidateEmail directive is left out and the ctrl.$valid method of the eaValidateUnique directive is always passing even though ctrl.$valid is false in console.log.
I have read through the AngularJS documentation, bought two books but the examples are always very basic. Currently I can't figure out where the problem could be located. It looks like there is a clash with ngModelController but I can't figure out the right way to solve this issue.
I'm currently testing with the ValidateCtrlNew form. So the field in the "New" section of the html form.
Questions:
Does anybody know how to write the directives so that they are executed in serial order as I add them as attributes to the input element?
How can I prevent such clashes with directives? Isolated scope is also no option for multiple directives.
Here is the jsfiddle: http://jsfiddle.net/charms/6j3U8/230/
<div ng-controller="ValidateCtrlNew">
<form name="user_form_new" class="pure-form" novalidate>
<fieldset>
<legend>New</legend>
<input type="text" name="email" ng-model="user.email" placeholder="E-Mail" class="txt_fld" ng-required="true" ea-validate-email ea-validate-unique/><br/>
<div class="inv_msg" ng-show="user_form_new.email.$dirty && user_form_new.email.$invalid">Invalid:
<span ng-show="user_form_new.email.$error.required">Please enter your email.</span>
<span ng-show="user_form_new.email.$error.eaValidateEmail">This is not a valid email.</span>
<span ng-show="user_form_new.email.$error.eaValidateEmailCheck">Checking email....</span>
<span ng-show="user_form_new.email.$error.eaValidateUnique">This email is already taken.</span>
</div>
</fieldset>
</form>
</div>
.directive('eaValidateUnique', ['$http', function($http) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
ctrl.$parsers.push(function(viewValue) {
console.log(ctrl);
//ctrl.$setValidity('eaValidateUnique', true);
if(ctrl.$valid) {
ctrl.$setValidity('eaValidateUnique', false);
console.log("valid was true");
}
});
}
};
}])
.directive('eaValidateEmail', [function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
var EMAIL_REGEXP = /^([\w-\.]+)#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;
ctrl.$parsers.push(function(viewValue) {
// set validity to true to clear out if previous validators fail
ctrl.$setValidity('eaValidateEmail', true);
if(ctrl.$valid) {
// set validity to false as we need to check the value here
ctrl.$setValidity('eaValidateEmail', false);
if(viewValue !== undefined && viewValue !== "" && EMAIL_REGEXP.test(viewValue)) {
// if the format of the email is valid then we set validity to true
ctrl.$setValidity('eaValidateEmail', true);
ctrl.$setValidity('eaValidateEmailCheck', true);
console.log("TRUE");
} else {
// if the format of the email is invalid we set validity to false
ctrl.$setValidity('eaValidateEmail', false);
ctrl.$setValidity('eaValidateEmailCheck', true);
console.log("FALSE");
}
}
return viewValue;
});
}
};
}]);
you can add priority to eaValidateEmail to 100 like..
restrict: 'A',
priority:'100',
require: 'ngModel',
From what I understand is that the validators are chained on the $parsers array. AngularJs validators return the value if the validator decides the value if valid, but return an undefined if the value is invalid.
This way, the other validators on your chain would not have a value to work with anymore.
http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController#$parsers
Angularjs is running my forms through the FormController (eg tracking pristine, dirty, etc). I don't need this functionality; I'm sure it's adding overhead to my $digests.
How can I shut it off?
AFAIK there is no simple switch to turn off AngularJS validation. Actually most of the validation happens in the NgModelController and input directives - basically code in the input.js file. So, to get rid of the built-in validation you would have to re-develop code from this file (plus some others, like select).
Did you identify validation code as a performance bottleneck in your application?
UPDATE : This does NOT work ... well at least not in a way you'd like it to. Adding ng-non-bindable to the form or any input breaks ALL binding. So, your ng-model in the inputs won't work anymore. Sorry ....
ng-non-bindable is the solution to this problem.
It will prevent AngularJS from seeing the form as a directive. This will make AngularJS ignore the entire form:
<form name="inviteContactForm" ng-non-bindable>
This will make AngularJS ignore one part of a form:
<input type="email" name="email" ng-non-bindable>
You can read a bit about my whining on this issue here. http://calendee.com/preventing-angularjs-from-hijacking-forms/
Internally Angular creates factories out of directives by adding the Directive suffix to the directive name. So you can replace the validation and input directive factories with no-operational ones.
var noopDirective = function() { return function () {}; };
angular.module('myModule')
.factory('requiredDirective', noopDirective)
.factory('ngRequiredDirective', noopDirective)
.factory('inputDirective', noopDirective)
.factory('textareaDirective', noopDirective); // etc...
Similar to previous answer from #Chui Tey, having a directive that requires 'ngModel'. This is what I did
for disabling validations in runtime:
//disabling all validators
forEach(ngModelController.$validators, function(validator, validatorName){
ngModelController.$setValidity(validatorName, true);//mark invalid as valid
var originalValidator = ngModelController.$validators[validatorName]; //we save the original validator for being able to restore it in future
ngModelController.$validators[validatorName] = _.wrap(true);//overwrite the validator
ngModelController.$validators[validatorName].originalValidator = originalValidator;
});
//for restoring validations
forEach(ngModelController.$validators, function(validator, validatorName){
if(ngModelController.$validators[validatorName].originalValidator){
ngModelController.$validators[validatorName] = ngModelController.$validators[validatorName].originalValidator;
}
});
ngModelController.$validate(); //triger validations
A colleague suggested a nifty way of disabling validators. Here's an implementation:
<input type="radio" name="enableValidation" ng-model="$ctrl.validationEnabled" ng-value="true" />Enabled
<input type="radio" name="enableValidation" ng-model="$ctrl.validationEnabled" ng-value="false" />Disabled
<input type="number"
name="age"
ng-model="$ctrl.age"
min="20"
disable-validation="!$ctrl.validationEnabled" />
When disable-validation is true, then all validation rules automatically passes.
function disableValidation(scope, elem, attrs, ngModelController) {
function wrapOriginalValidators() {
var originalValidators = angular.copy(ngModelController.$validators);
Object.keys(originalValidators).forEach(function(key) {
ngModelController.$validators[key] = function(modelValue, viewValue) {
return scope.$eval(attrs.disableValidation) || originalValidators[key](modelValue, viewValue);
}
});
}
function watchDisableCriteria() {
scope.$watch(attrs.disableValidation, function() {
// trigger validation
var originalViewValue = ngModelController.$viewValue;
scope.$applyAsync(function() {
ngModelController.$setViewValue('');
});
scope.$applyAsync(function() {
ngModelController.$setViewValue(originalViewValue);
});
});
}
wrapOriginalValidators();
watchDisableCriteria();
}
angular.module('app', [])
.directive('disableValidation', function() {
return {
require: 'ngModel',
link: disableValidation
};
});
Obviously, you'd not use this for performance reasons, but when you
need to dynamically enable or disable validations.
Sample: https://plnkr.co/edit/EM1tGb
I have a form with input fields and validation setup by adding the required attributes and such. But for some fields I need to do some extra validation. How would I "tap in" to the validation that FormController controls?
Custom validation could be something like "if these 3 fields are filled in, then this field is required and needs to be formatted in a particular way".
There's a method in FormController.$setValidity but that doesn't look like a public API so I rather not use it. Creating a custom directive and using NgModelController looks like another option, but would basically require me to create a directive for each custom validation rule, which I do not want.
Actually, marking a field from the controller as invalid (while also keeping FormController in sync) might be the thing that I need in the simplest scenario to get the job done, but I don't know how to do that.
Edit: added information about ngMessages (>= 1.3.X) below.
Standard form validation messages (1.0.X and above)
Since this is one of the top results if you Google "Angular Form Validation", currently, I want to add another answer to this for anyone coming in from there.
There's a method in FormController.$setValidity but that doesn't look like a public API so I rather not use it.
It's "public", no worries. Use it. That's what it's for. If it weren't meant to be used, the Angular devs would have privatized it in a closure.
To do custom validation, if you don't want to use Angular-UI as the other answer suggested, you can simply roll your own validation directive.
app.directive('blacklist', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
var blacklist = attr.blacklist.split(',');
//For DOM -> model validation
ngModel.$parsers.unshift(function(value) {
var valid = blacklist.indexOf(value) === -1;
ngModel.$setValidity('blacklist', valid);
return valid ? value : undefined;
});
//For model -> DOM validation
ngModel.$formatters.unshift(function(value) {
ngModel.$setValidity('blacklist', blacklist.indexOf(value) === -1);
return value;
});
}
};
});
And here's some example usage:
<form name="myForm" ng-submit="doSomething()">
<input type="text" name="fruitName" ng-model="data.fruitName" blacklist="coconuts,bananas,pears" required/>
<span ng-show="myForm.fruitName.$error.blacklist">
The phrase "{{data.fruitName}}" is blacklisted</span>
<span ng-show="myForm.fruitName.$error.required">required</span>
<button type="submit" ng-disabled="myForm.$invalid">Submit</button>
</form>
Note: in 1.2.X it's probably preferrable to substitute ng-if for ng-show above
Here is an obligatory plunker link
Also, I've written a few blog entries about just this subject that goes into a little more detail:
Angular Form Validation
Custom Validation Directives
Edit: using ngMessages in 1.3.X
You can now use the ngMessages module instead of ngShow to show your error messages. It will actually work with anything, it doesn't have to be an error message, but here's the basics:
Include <script src="angular-messages.js"></script>
Reference ngMessages in your module declaration:
var app = angular.module('myApp', ['ngMessages']);
Add the appropriate markup:
<form name="personForm">
<input type="email" name="email" ng-model="person.email" required/>
<div ng-messages="personForm.email.$error">
<div ng-message="required">required</div>
<div ng-message="email">invalid email</div>
</div>
</form>
In the above markup, ng-message="personForm.email.$error" basically specifies a context for the ng-message child directives. Then ng-message="required" and ng-message="email" specify properties on that context to watch. Most importantly, they also specify an order to check them in. The first one it finds in the list that is "truthy" wins, and it will show that message and none of the others.
And a plunker for the ngMessages example
Angular-UI's project includes a ui-validate directive, which will probably help you with this. It let's you specify a function to call to do the validation.
Have a look at the demo page: http://angular-ui.github.com/, search down to the Validate heading.
From the demo page:
<input ng-model="email" ui-validate='{blacklist : notBlackListed}'>
<span ng-show='form.email.$error.blacklist'>This e-mail is black-listed!</span>
then in your controller:
function ValidateCtrl($scope) {
$scope.blackList = ['bad#domain.example','verybad#domain.example'];
$scope.notBlackListed = function(value) {
return $scope.blackList.indexOf(value) === -1;
};
}
You can use ng-required for your validation scenario ("if these 3 fields are filled in, then this field is required":
<div ng-app>
<input type="text" ng-model="field1" placeholder="Field1">
<input type="text" ng-model="field2" placeholder="Field2">
<input type="text" ng-model="field3" placeholder="Field3">
<input type="text" ng-model="dependentField" placeholder="Custom validation"
ng-required="field1 && field2 && field3">
</div>
You can use Angular-Validator.
Example: using a function to validate a field
<input type = "text"
name = "firstName"
ng-model = "person.firstName"
validator = "myCustomValidationFunction(form.firstName)">
Then in your controller you would have something like
$scope.myCustomValidationFunction = function(firstName){
if ( firstName === "John") {
return true;
}
You can also do something like this:
<input type = "text"
name = "firstName"
ng-model = "person.firstName"
validator = "'!(field1 && field2 && field3)'"
invalid-message = "'This field is required'">
(where field1 field2, and field3 are scope variables. You might also want to check if the fields do not equal the empty string)
If the field does not pass the validator then the field will be marked as invalid and the user will not be able to submit the form.
For more use cases and examples see: https://github.com/turinggroup/angular-validator
Disclaimer: I am the author of Angular-Validator
I recently created a directive to allow for expression-based invalidation of angular form inputs. Any valid angular expression can be used, and it supports custom validation keys using object notation. Tested with angular v1.3.8
.directive('invalidIf', [function () {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var argsObject = scope.$eval(attrs.invalidIf);
if (!angular.isObject(argsObject)) {
argsObject = { invalidIf: attrs.invalidIf };
}
for (var validationKey in argsObject) {
scope.$watch(argsObject[validationKey], function (newVal) {
ctrl.$setValidity(validationKey, !newVal);
});
}
}
};
}]);
You can use it like this:
<input ng-model="foo" invalid-if="{fooIsGreaterThanBar: 'foo > bar',
fooEqualsSomeFuncResult: 'foo == someFuncResult()'}/>
Or by just passing in an expression (it will be given the default validationKey of "invalidIf")
<input ng-model="foo" invalid-if="foo > bar"/>
Here's a cool way to do custom wildcard expression validations in a form (from: Advanced form validation with AngularJS and filters):
<form novalidate="">
<input type="text" id="name" name="name" ng-model="newPerson.name"
ensure-expression="(persons | filter:{name: newPerson.name}:true).length !== 1">
<!-- or in your case:-->
<input type="text" id="fruitName" name="fruitName" ng-model="data.fruitName"
ensure-expression="(blacklist | filter:{fruitName: data.fruitName}:true).length !== 1">
</form>
app.directive('ensureExpression', ['$http', '$parse', function($http, $parse) {
return {
require: 'ngModel',
link: function(scope, ele, attrs, ngModelController) {
scope.$watch(attrs.ngModel, function(value) {
var booleanResult = $parse(attrs.ensureExpression)(scope);
ngModelController.$setValidity('expression', booleanResult);
});
}
};
}]);
jsFiddle demo (supports expression naming and multiple expressions)
It's similar to ui-validate, but you don't need a scope specific validation function (this works generically) and ofcourse you don't need ui.utils this way.
#synergetic I think #blesh suppose to put function validate as below
function validate(value) {
var valid = blacklist.indexOf(value) === -1;
ngModel.$setValidity('blacklist', valid);
return valid ? value : undefined;
}
ngModel.$formatters.unshift(validate);
ngModel.$parsers.unshift(validate);
Update:
Improved and simplified version of previous directive (one instead of two) with same functionality:
.directive('myTestExpression', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
var expr = attrs.myTestExpression;
var watches = attrs.myTestExpressionWatch;
ctrl.$validators.mytestexpression = function (modelValue, viewValue) {
return expr == undefined || (angular.isString(expr) && expr.length < 1) || $parse(expr)(scope, { $model: modelValue, $view: viewValue }) === true;
};
if (angular.isString(watches)) {
angular.forEach(watches.split(",").filter(function (n) { return !!n; }), function (n) {
scope.$watch(n, function () {
ctrl.$validate();
});
});
}
}
};
}])
Example usage:
<input ng-model="price1"
my-test-expression="$model > 0"
my-test-expression-watch="price2,someOtherWatchedPrice" />
<input ng-model="price2"
my-test-expression="$model > 10"
my-test-expression-watch="price1"
required />
Result: Mutually dependent test expressions where validators are executed on change of other's directive model and current model.
Test expression has local $model variable which you should use to compare it to other variables.
Previously:
I've made an attempt to improve #Plantface code by adding extra directive. This extra directive very useful if our expression needs to be executed when changes are made in more than one ngModel variables.
.directive('ensureExpression', ['$parse', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
controller: function () { },
scope: true,
link: function (scope, element, attrs, ngModelCtrl) {
scope.validate = function () {
var booleanResult = $parse(attrs.ensureExpression)(scope);
ngModelCtrl.$setValidity('expression', booleanResult);
};
scope.$watch(attrs.ngModel, function(value) {
scope.validate();
});
}
};
}])
.directive('ensureWatch', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ensureExpression',
link: function (scope, element, attrs, ctrl) {
angular.forEach(attrs.ensureWatch.split(",").filter(function (n) { return !!n; }), function (n) {
scope.$watch(n, function () {
scope.validate();
});
});
}
};
}])
Example how to use it to make cross validated fields:
<input name="price1"
ng-model="price1"
ensure-expression="price1 > price2"
ensure-watch="price2" />
<input name="price2"
ng-model="price2"
ensure-expression="price2 > price3"
ensure-watch="price3" />
<input name="price3"
ng-model="price3"
ensure-expression="price3 > price1 && price3 > price2"
ensure-watch="price1,price2" />
ensure-expression is executed to validate model when ng-model or any of ensure-watch variables is changed.
Custom Validations that call a Server
Use the ngModelController $asyncValidators API which handles asynchronous validation, such as making an $http request to the backend. Functions added to the object must return a promise that must be resolved when valid or rejected when invalid. In-progress async validations are stored by key in ngModelController.$pending. For more information, see AngularJS Developer Guide - Forms (Custom Validation).
ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;
// Lookup user by username
return $http.get('/api/users/' + value).
then(function resolved() {
//username exists, this means validation fails
return $q.reject('exists');
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
});
};
For more information, see
ngModelController $asyncValidators API
AngularJS Developer Guide - Forms (Custom Validation).
Using the $validators API
The accepted answer uses the $parsers and $formatters pipelines to add a custom synchronous validator. AngularJS 1.3+ added a $validators API so there is no need to put validators in the $parsers and $formatters pipelines:
app.directive('blacklist', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
ngModel.$validators.blacklist = function(modelValue, viewValue) {
var blacklist = attr.blacklist.split(',');
var value = modelValue || viewValue;
var valid = blacklist.indexOf(value) === -1;
return valid;
});
}
};
});
For more information, see AngularJS ngModelController API Reference - $validators.
In AngularJS the best place to define Custom Validation is Cutsom directive.
AngularJS provide a ngMessages module.
ngMessages is a directive that is designed to show and hide messages
based on the state of a key/value object that it listens on. The
directive itself complements error message reporting with the ngModel
$error object (which stores a key/value state of validation errors).
For custom form validation One should use ngMessages Modules with custom directive.Here i have a simple validation which will check if number length is less then 6 display an error on screen
<form name="myform" novalidate>
<table>
<tr>
<td><input name='test' type='text' required ng-model='test' custom-validation></td>
<td ng-messages="myform.test.$error"><span ng-message="invalidshrt">Too Short</span></td>
</tr>
</table>
</form>
Here is how to create custom validation directive
angular.module('myApp',['ngMessages']);
angular.module('myApp',['ngMessages']).directive('customValidation',function(){
return{
restrict:'A',
require: 'ngModel',
link:function (scope, element, attr, ctrl) {// 4th argument contain model information
function validationError(value) // you can use any function and parameter name
{
if (value.length > 6) // if model length is greater then 6 it is valide state
{
ctrl.$setValidity('invalidshrt',true);
}
else
{
ctrl.$setValidity('invalidshrt',false) //if less then 6 is invalide
}
return value; //return to display error
}
ctrl.$parsers.push(validationError); //parsers change how view values will be saved in the model
}
};
});
$setValidity is inbuilt function to set model state to valid/invalid
I extended #Ben Lesh's answer with an ability to specify whether the validation is case sensitive or not (default)
use:
<input type="text" name="fruitName" ng-model="data.fruitName" blacklist="Coconuts,Bananas,Pears" caseSensitive="true" required/>
code:
angular.module('crm.directives', []).
directive('blacklist', [
function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
'blacklist': '=',
},
link: function ($scope, $elem, $attrs, modelCtrl) {
var check = function (value) {
if (!$attrs.casesensitive) {
value = (value && value.toUpperCase) ? value.toUpperCase() : value;
$scope.blacklist = _.map($scope.blacklist, function (item) {
return (item.toUpperCase) ? item.toUpperCase() : item
})
}
return !_.isArray($scope.blacklist) || $scope.blacklist.indexOf(value) === -1;
}
//For DOM -> model validation
modelCtrl.$parsers.unshift(function (value) {
var valid = check(value);
modelCtrl.$setValidity('blacklist', valid);
return value;
});
//For model -> DOM validation
modelCtrl.$formatters.unshift(function (value) {
modelCtrl.$setValidity('blacklist', check(value));
return value;
});
}
};
}
]);
Some great examples and libs presented in this thread, but they didn't quite have what I was looking for. My approach: angular-validity -- a promise based validation lib for asynchronous validation, with optional Bootstrap styling baked-in.
An angular-validity solution for the OP's use case might look something like this:
<input type="text" name="field4" ng-model="field4"
validity="eval"
validity-eval="!(field1 && field2 && field3 && !field4)"
validity-message-eval="This field is required">
Here's a Fiddle, if you want to take it for a spin. The lib is available on GitHub, has detailed documentation, and plenty of live demos.