@@ -1386,7 +1386,8 @@ var VALID_CLASS = 'ng-valid',
13861386 PRISTINE_CLASS = 'ng-pristine' ,
13871387 DIRTY_CLASS = 'ng-dirty' ,
13881388 UNTOUCHED_CLASS = 'ng-untouched' ,
1389- TOUCHED_CLASS = 'ng-touched' ;
1389+ TOUCHED_CLASS = 'ng-touched' ,
1390+ PENDING_CLASS = 'ng-pending' ;
13901391
13911392/**
13921393 * @ngdoc type
@@ -1421,6 +1422,44 @@ var VALID_CLASS = 'ng-valid',
14211422 * provided with the model value as an argument and must return a true or false value depending
14221423 * on the response of that validation.
14231424 *
1425+ * ```js
1426+ * ngModel.$validators.validCharacters = function(modelValue, viewValue) {
1427+ * var value = modelValue || viewValue;
1428+ * return /[0-9]+/.test(value) &&
1429+ * /[a-z]+/.test(value) &&
1430+ * /[A-Z]+/.test(value) &&
1431+ * /\W+/.test(value);
1432+ * };
1433+ * ```
1434+ *
1435+ * @property {Object.<string, function> } $asyncValidators A collection of validations that are expected to
1436+ * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided
1437+ * is expected to return a promise when it is run during the model validation process. Once the promise
1438+ * is delivered then the validation status will be set to true when fulfilled and false when rejected.
1439+ * When the asynchronous validators are trigged, each of the validators will run in parallel and the model
1440+ * value will only be updated once all validators have been fulfilled. Also, keep in mind that all
1441+ * asynchronous validators will only run once all synchronous validators have passed.
1442+ *
1443+ * Please note that if $http is used then it is important that the server returns a success HTTP response code
1444+ * in order to fulfill the validation and a status level of `4xx` in order to reject the validation.
1445+ *
1446+ * ```js
1447+ * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
1448+ * var value = modelValue || viewValue;
1449+ * return $http.get('/api/users/' + value).
1450+ * then(function() {
1451+ * //username exists, this means the validator fails
1452+ * return false;
1453+ * }, function() {
1454+ * //username does not exist, therefore this validation is true
1455+ * return true;
1456+ * });
1457+ * };
1458+ * ```
1459+ *
1460+ * @param {string } name The name of the validator.
1461+ * @param {Function } validationFn The validation function that will be run.
1462+ *
14241463 * @property {Array.<Function> } $viewChangeListeners Array of functions to execute whenever the
14251464 * view value has changed. It is called with no arguments, and its return value is ignored.
14261465 * This can be used in place of additional $watches against the model value.
@@ -1433,6 +1472,7 @@ var VALID_CLASS = 'ng-valid',
14331472 * @property {boolean } $dirty True if user has already interacted with the control.
14341473 * @property {boolean } $valid True if there is no error.
14351474 * @property {boolean } $invalid True if at least one error on the control.
1475+ * @property {Object.<string, boolean> } $pending True if one or more asynchronous validators is still yet to be delivered.
14361476 *
14371477 * @description
14381478 *
@@ -1540,6 +1580,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
15401580 this . $viewValue = Number . NaN ;
15411581 this . $modelValue = Number . NaN ;
15421582 this . $validators = { } ;
1583+ this . $asyncValidators = { } ;
1584+ this . $validators = { } ;
15431585 this . $parsers = [ ] ;
15441586 this . $formatters = [ ] ;
15451587 this . $viewChangeListeners = [ ] ;
@@ -1607,6 +1649,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16071649
16081650 var parentForm = $element . inheritedData ( '$formController' ) || nullFormCtrl ,
16091651 invalidCount = 0 , // used to easily determine if we are valid
1652+ pendingCount = 0 , // used to easily determine if there are any pending validations
16101653 $error = this . $error = { } ; // keep invalid keys here
16111654
16121655
@@ -1624,18 +1667,67 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16241667 }
16251668
16261669 this . $$clearValidity = function ( ) {
1670+ $animate . removeClass ( $element , PENDING_CLASS ) ;
16271671 forEach ( ctrl . $error , function ( val , key ) {
16281672 var validationKey = snake_case ( key , '-' ) ;
16291673 $animate . removeClass ( $element , VALID_CLASS + validationKey ) ;
16301674 $animate . removeClass ( $element , INVALID_CLASS + validationKey ) ;
16311675 } ) ;
16321676
1677+ // just incase an asnyc validator is still running while
1678+ // the parser fails
1679+ if ( ctrl . $pending ) {
1680+ ctrl . $$clearPending ( ) ;
1681+ }
1682+
16331683 invalidCount = 0 ;
16341684 $error = ctrl . $error = { } ;
16351685
16361686 parentForm . $$clearControlValidity ( ctrl ) ;
16371687 } ;
16381688
1689+ this . $$clearPending = function ( ) {
1690+ pendingCount = 0 ;
1691+ ctrl . $pending = undefined ;
1692+ $animate . removeClass ( $element , PENDING_CLASS ) ;
1693+ } ;
1694+
1695+ this . $$setPending = function ( validationErrorKey , promise , currentValue ) {
1696+ ctrl . $pending = ctrl . $pending || { } ;
1697+ if ( angular . isUndefined ( ctrl . $pending [ validationErrorKey ] ) ) {
1698+ ctrl . $pending [ validationErrorKey ] = true ;
1699+ pendingCount ++ ;
1700+ }
1701+
1702+ ctrl . $valid = ctrl . $invalid = undefined ;
1703+ parentForm . $$setPending ( validationErrorKey , ctrl ) ;
1704+
1705+ $animate . addClass ( $element , PENDING_CLASS ) ;
1706+ $animate . removeClass ( $element , INVALID_CLASS ) ;
1707+ $animate . removeClass ( $element , VALID_CLASS ) ;
1708+
1709+ //Special-case for (undefined|null|false|NaN) values to avoid
1710+ //having to compare each of them with each other
1711+ currentValue = currentValue || '' ;
1712+ promise . then ( resolve ( true ) , resolve ( false ) ) ;
1713+
1714+ function resolve ( bool ) {
1715+ return function ( ) {
1716+ var value = ctrl . $viewValue || '' ;
1717+ if ( ctrl . $pending && ctrl . $pending [ validationErrorKey ] && currentValue === value ) {
1718+ pendingCount -- ;
1719+ delete ctrl . $pending [ validationErrorKey ] ;
1720+ ctrl . $setValidity ( validationErrorKey , bool ) ;
1721+ if ( pendingCount === 0 ) {
1722+ ctrl . $$clearPending ( ) ;
1723+ ctrl . $$updateValidModelValue ( value ) ;
1724+ ctrl . $$writeModelToScope ( ) ;
1725+ }
1726+ }
1727+ } ;
1728+ }
1729+ } ;
1730+
16391731 /**
16401732 * @ngdoc method
16411733 * @name ngModel.NgModelController#$setValidity
@@ -1655,28 +1747,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16551747 * @param {boolean } isValid Whether the current state is valid (true) or invalid (false).
16561748 */
16571749 this . $setValidity = function ( validationErrorKey , isValid ) {
1658- // Purposeful use of ! here to cast isValid to boolean in case it is undefined
1750+
1751+ // avoid doing anything if the validation value has not changed
16591752 // jshint -W018
1660- if ( $error [ validationErrorKey ] === ! isValid ) return ;
1753+ if ( ! ctrl . $pending && $error [ validationErrorKey ] === ! isValid ) return ;
16611754 // jshint +W018
16621755
16631756 if ( isValid ) {
16641757 if ( $error [ validationErrorKey ] ) invalidCount -- ;
1665- if ( ! invalidCount ) {
1758+ if ( ! invalidCount && ! pendingCount ) {
16661759 toggleValidCss ( true ) ;
16671760 ctrl . $valid = true ;
16681761 ctrl . $invalid = false ;
16691762 }
16701763 } else if ( ! $error [ validationErrorKey ] ) {
1671- toggleValidCss ( false ) ;
1672- ctrl . $invalid = true ;
1673- ctrl . $valid = false ;
16741764 invalidCount ++ ;
1765+ if ( ! pendingCount ) {
1766+ toggleValidCss ( false ) ;
1767+ ctrl . $invalid = true ;
1768+ ctrl . $valid = false ;
1769+ }
16751770 }
16761771
16771772 $error [ validationErrorKey ] = ! isValid ;
16781773 toggleValidCss ( isValid , validationErrorKey ) ;
1679-
16801774 parentForm . $setValidity ( validationErrorKey , isValid , ctrl ) ;
16811775 } ;
16821776
@@ -1804,7 +1898,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18041898 * @name ngModel.NgModelController#$validate
18051899 *
18061900 * @description
1807- * Runs each of the registered validations set on the $ validators object .
1901+ * Runs each of the registered validators (first synchronous validators and then asynchronous validators) .
18081902 */
18091903 this . $validate = function ( ) {
18101904 // ignore $validate before model initialized
@@ -1820,9 +1914,40 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18201914 } ;
18211915
18221916 this . $$runValidators = function ( modelValue , viewValue ) {
1823- forEach ( ctrl . $validators , function ( fn , name ) {
1824- ctrl . $setValidity ( name , fn ( modelValue , viewValue ) ) ;
1917+ // this is called in the event if incase the input value changes
1918+ // while a former asynchronous validator is still doing its thing
1919+ if ( ctrl . $pending ) {
1920+ ctrl . $$clearPending ( ) ;
1921+ }
1922+
1923+ var continueValidation = validate ( ctrl . $validators , function ( validator , result ) {
1924+ ctrl . $setValidity ( validator , result ) ;
18251925 } ) ;
1926+
1927+ if ( continueValidation ) {
1928+ validate ( ctrl . $asyncValidators , function ( validator , result ) {
1929+ if ( ! isPromiseLike ( result ) ) {
1930+ throw $ngModelMinErr ( "$asyncValidators" ,
1931+ "Expected asynchronous validator to return a promise but got '{0}' instead." , result ) ;
1932+ }
1933+ ctrl . $$setPending ( validator , result , modelValue ) ;
1934+ } ) ;
1935+ }
1936+
1937+ ctrl . $$updateValidModelValue ( modelValue ) ;
1938+
1939+ function validate ( validators , callback ) {
1940+ var status = true ;
1941+ forEach ( validators , function ( fn , name ) {
1942+ var result = fn ( modelValue , viewValue ) ;
1943+ callback ( name , result ) ;
1944+ status = status && result ;
1945+ } ) ;
1946+ return status ;
1947+ }
1948+ } ;
1949+
1950+ this . $$updateValidModelValue = function ( modelValue ) {
18261951 ctrl . $modelValue = ctrl . $valid ? modelValue : undefined ;
18271952 ctrl . $$invalidModelValue = ctrl . $valid ? undefined : modelValue ;
18281953 } ;
@@ -1870,13 +1995,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18701995 ctrl . $$invalidModelValue = ctrl . $modelValue = undefined ;
18711996 ctrl . $$clearValidity ( ) ;
18721997 ctrl . $setValidity ( parserName , false ) ;
1998+ ctrl . $$writeModelToScope ( ) ;
18731999 } else if ( ctrl . $modelValue !== modelValue &&
18742000 ( isUndefined ( ctrl . $$invalidModelValue ) || ctrl . $$invalidModelValue != modelValue ) ) {
18752001 ctrl . $setValidity ( parserName , true ) ;
18762002 ctrl . $$runValidators ( modelValue , viewValue ) ;
2003+ ctrl . $$writeModelToScope ( ) ;
18772004 }
1878-
1879- ctrl . $$writeModelToScope ( ) ;
18802005 } ;
18812006
18822007 this . $$writeModelToScope = function ( ) {
0 commit comments