@@ -1365,7 +1365,8 @@ var VALID_CLASS = 'ng-valid',
13651365 PRISTINE_CLASS = 'ng-pristine' ,
13661366 DIRTY_CLASS = 'ng-dirty' ,
13671367 UNTOUCHED_CLASS = 'ng-untouched' ,
1368- TOUCHED_CLASS = 'ng-touched' ;
1368+ TOUCHED_CLASS = 'ng-touched' ,
1369+ PENDING_CLASS = 'ng-pending' ;
13691370
13701371/**
13711372 * @ngdoc type
@@ -1400,6 +1401,44 @@ var VALID_CLASS = 'ng-valid',
14001401 * provided with the model value as an argument and must return a true or false value depending
14011402 * on the response of that validation.
14021403 *
1404+ * ```js
1405+ * ngModel.$validators.validCharacters = function(modelValue, viewValue) {
1406+ * var value = modelValue || viewValue;
1407+ * return /[0-9]+/.test(value) &&
1408+ * /[a-z]+/.test(value) &&
1409+ * /[A-Z]+/.test(value) &&
1410+ * /\W+/.test(value);
1411+ * };
1412+ * ```
1413+ *
1414+ * @property {Object.<string, function> } $asyncValidators A collection of validations that are expected to
1415+ * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided
1416+ * is expected to return a promise when it is run during the model validation process. Once the promise
1417+ * is delivered then the validation status will be set to true when fulfilled and false when rejected.
1418+ * When the asynchronous validators are trigged, each of the validators will run in parallel and the model
1419+ * value will only be updated once all validators have been fulfilled. Also, keep in mind that all
1420+ * asynchronous validators will only run once all synchronous validators have passed.
1421+ *
1422+ * Please note that if $http is used then it is important that the server returns a success HTTP response code
1423+ * in order to fulfill the validation and a status level of `4xx` in order to reject the validation.
1424+ *
1425+ * ```js
1426+ * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
1427+ * var value = modelValue || viewValue;
1428+ * return $http.get('/api/users/' + value).
1429+ * then(function() {
1430+ * //username exists, this means the validator fails
1431+ * return false;
1432+ * }, function() {
1433+ * //username does not exist, therefore this validation is true
1434+ * return true;
1435+ * });
1436+ * };
1437+ * ```
1438+ *
1439+ * @param {string } name The name of the validator.
1440+ * @param {Function } validationFn The validation function that will be run.
1441+ *
14031442 * @property {Array.<Function> } $viewChangeListeners Array of functions to execute whenever the
14041443 * view value has changed. It is called with no arguments, and its return value is ignored.
14051444 * This can be used in place of additional $watches against the model value.
@@ -1412,6 +1451,7 @@ var VALID_CLASS = 'ng-valid',
14121451 * @property {boolean } $dirty True if user has already interacted with the control.
14131452 * @property {boolean } $valid True if there is no error.
14141453 * @property {boolean } $invalid True if at least one error on the control.
1454+ * @property {Object.<string, boolean> } $pending True if one or more asynchronous validators is still yet to be delivered.
14151455 *
14161456 * @description
14171457 *
@@ -1519,6 +1559,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
15191559 this . $viewValue = Number . NaN ;
15201560 this . $modelValue = Number . NaN ;
15211561 this . $validators = { } ;
1562+ this . $asyncValidators = { } ;
1563+ this . $validators = { } ;
15221564 this . $parsers = [ ] ;
15231565 this . $formatters = [ ] ;
15241566 this . $viewChangeListeners = [ ] ;
@@ -1586,6 +1628,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
15861628
15871629 var parentForm = $element . inheritedData ( '$formController' ) || nullFormCtrl ,
15881630 invalidCount = 0 , // used to easily determine if we are valid
1631+ pendingCount = 0 , // used to easily determine if there are any pending validations
15891632 $error = this . $error = { } ; // keep invalid keys here
15901633
15911634
@@ -1603,18 +1646,67 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16031646 }
16041647
16051648 this . $$clearValidity = function ( ) {
1649+ $animate . removeClass ( $element , PENDING_CLASS ) ;
16061650 forEach ( ctrl . $error , function ( val , key ) {
16071651 var validationKey = snake_case ( key , '-' ) ;
16081652 $animate . removeClass ( $element , VALID_CLASS + validationKey ) ;
16091653 $animate . removeClass ( $element , INVALID_CLASS + validationKey ) ;
16101654 } ) ;
16111655
1656+ // just incase an asnyc validator is still running while
1657+ // the parser fails
1658+ if ( ctrl . $pending ) {
1659+ ctrl . $$clearPending ( ) ;
1660+ }
1661+
16121662 invalidCount = 0 ;
16131663 $error = ctrl . $error = { } ;
16141664
16151665 parentForm . $$clearControlValidity ( ctrl ) ;
16161666 } ;
16171667
1668+ this . $$clearPending = function ( ) {
1669+ pendingCount = 0 ;
1670+ ctrl . $pending = undefined ;
1671+ $animate . removeClass ( $element , PENDING_CLASS ) ;
1672+ } ;
1673+
1674+ this . $$setPending = function ( validationErrorKey , promise , currentValue ) {
1675+ ctrl . $pending = ctrl . $pending || { } ;
1676+ if ( angular . isUndefined ( ctrl . $pending [ validationErrorKey ] ) ) {
1677+ ctrl . $pending [ validationErrorKey ] = true ;
1678+ pendingCount ++ ;
1679+ }
1680+
1681+ ctrl . $valid = ctrl . $invalid = undefined ;
1682+ parentForm . $$setPending ( validationErrorKey , ctrl ) ;
1683+
1684+ $animate . addClass ( $element , PENDING_CLASS ) ;
1685+ $animate . removeClass ( $element , INVALID_CLASS ) ;
1686+ $animate . removeClass ( $element , VALID_CLASS ) ;
1687+
1688+ //Special-case for (undefined|null|false|NaN) values to avoid
1689+ //having to compare each of them with each other
1690+ currentValue = currentValue || '' ;
1691+ promise . then ( resolve ( true ) , resolve ( false ) ) ;
1692+
1693+ function resolve ( bool ) {
1694+ return function ( ) {
1695+ var value = ctrl . $viewValue || '' ;
1696+ if ( ctrl . $pending && ctrl . $pending [ validationErrorKey ] && currentValue === value ) {
1697+ pendingCount -- ;
1698+ delete ctrl . $pending [ validationErrorKey ] ;
1699+ ctrl . $setValidity ( validationErrorKey , bool ) ;
1700+ if ( pendingCount === 0 ) {
1701+ ctrl . $$clearPending ( ) ;
1702+ ctrl . $$updateValidModelValue ( value ) ;
1703+ ctrl . $$writeModelToScope ( ) ;
1704+ }
1705+ }
1706+ } ;
1707+ }
1708+ } ;
1709+
16181710 /**
16191711 * @ngdoc method
16201712 * @name ngModel.NgModelController#$setValidity
@@ -1634,28 +1726,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16341726 * @param {boolean } isValid Whether the current state is valid (true) or invalid (false).
16351727 */
16361728 this . $setValidity = function ( validationErrorKey , isValid ) {
1637- // Purposeful use of ! here to cast isValid to boolean in case it is undefined
1729+
1730+ // avoid doing anything if the validation value has not changed
16381731 // jshint -W018
1639- if ( $error [ validationErrorKey ] === ! isValid ) return ;
1732+ if ( ! ctrl . $pending && $error [ validationErrorKey ] === ! isValid ) return ;
16401733 // jshint +W018
16411734
16421735 if ( isValid ) {
16431736 if ( $error [ validationErrorKey ] ) invalidCount -- ;
1644- if ( ! invalidCount ) {
1737+ if ( ! invalidCount && ! pendingCount ) {
16451738 toggleValidCss ( true ) ;
16461739 ctrl . $valid = true ;
16471740 ctrl . $invalid = false ;
16481741 }
16491742 } else if ( ! $error [ validationErrorKey ] ) {
1650- toggleValidCss ( false ) ;
1651- ctrl . $invalid = true ;
1652- ctrl . $valid = false ;
16531743 invalidCount ++ ;
1744+ if ( ! pendingCount ) {
1745+ toggleValidCss ( false ) ;
1746+ ctrl . $invalid = true ;
1747+ ctrl . $valid = false ;
1748+ }
16541749 }
16551750
16561751 $error [ validationErrorKey ] = ! isValid ;
16571752 toggleValidCss ( isValid , validationErrorKey ) ;
1658-
16591753 parentForm . $setValidity ( validationErrorKey , isValid , ctrl ) ;
16601754 } ;
16611755
@@ -1783,7 +1877,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17831877 * @name ngModel.NgModelController#$validate
17841878 *
17851879 * @description
1786- * Runs each of the registered validations set on the $ validators object .
1880+ * Runs each of the registered validators (first synchronous validators and then asynchronous validators) .
17871881 */
17881882 this . $validate = function ( ) {
17891883 // ignore $validate before model initialized
@@ -1799,9 +1893,40 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17991893 } ;
18001894
18011895 this . $$runValidators = function ( modelValue , viewValue ) {
1802- forEach ( ctrl . $validators , function ( fn , name ) {
1803- ctrl . $setValidity ( name , fn ( modelValue , viewValue ) ) ;
1896+ // this is called in the event if incase the input value changes
1897+ // while a former asynchronous validator is still doing its thing
1898+ if ( ctrl . $pending ) {
1899+ ctrl . $$clearPending ( ) ;
1900+ }
1901+
1902+ var continueValidation = validate ( ctrl . $validators , function ( validator , result ) {
1903+ ctrl . $setValidity ( validator , result ) ;
18041904 } ) ;
1905+
1906+ if ( continueValidation ) {
1907+ validate ( ctrl . $asyncValidators , function ( validator , result ) {
1908+ if ( ! isPromiseLike ( result ) ) {
1909+ throw $ngModelMinErr ( "$asyncValidators" ,
1910+ "Expected asynchronous validator to return a promise but got '{0}' instead." , result ) ;
1911+ }
1912+ ctrl . $$setPending ( validator , result , modelValue ) ;
1913+ } ) ;
1914+ }
1915+
1916+ ctrl . $$updateValidModelValue ( modelValue ) ;
1917+
1918+ function validate ( validators , callback ) {
1919+ var status = true ;
1920+ forEach ( validators , function ( fn , name ) {
1921+ var result = fn ( modelValue , viewValue ) ;
1922+ callback ( name , result ) ;
1923+ status = status && result ;
1924+ } ) ;
1925+ return status ;
1926+ }
1927+ } ;
1928+
1929+ this . $$updateValidModelValue = function ( modelValue ) {
18051930 ctrl . $modelValue = ctrl . $valid ? modelValue : undefined ;
18061931 ctrl . $$invalidModelValue = ctrl . $valid ? undefined : modelValue ;
18071932 } ;
@@ -1849,13 +1974,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18491974 ctrl . $$invalidModelValue = ctrl . $modelValue = undefined ;
18501975 ctrl . $$clearValidity ( ) ;
18511976 ctrl . $setValidity ( parserName , false ) ;
1977+ ctrl . $$writeModelToScope ( ) ;
18521978 } else if ( ctrl . $modelValue !== modelValue &&
18531979 ( isUndefined ( ctrl . $$invalidModelValue ) || ctrl . $$invalidModelValue != modelValue ) ) {
18541980 ctrl . $setValidity ( parserName , true ) ;
18551981 ctrl . $$runValidators ( modelValue , viewValue ) ;
1982+ ctrl . $$writeModelToScope ( ) ;
18561983 }
1857-
1858- ctrl . $$writeModelToScope ( ) ;
18591984 } ;
18601985
18611986 this . $$writeModelToScope = function ( ) {
0 commit comments