@@ -1365,7 +1365,8 @@ var VALID_CLASS = 'ng-valid',
1365
1365
PRISTINE_CLASS = 'ng-pristine' ,
1366
1366
DIRTY_CLASS = 'ng-dirty' ,
1367
1367
UNTOUCHED_CLASS = 'ng-untouched' ,
1368
- TOUCHED_CLASS = 'ng-touched' ;
1368
+ TOUCHED_CLASS = 'ng-touched' ,
1369
+ PENDING_CLASS = 'ng-pending' ;
1369
1370
1370
1371
/**
1371
1372
* @ngdoc type
@@ -1400,6 +1401,44 @@ var VALID_CLASS = 'ng-valid',
1400
1401
* provided with the model value as an argument and must return a true or false value depending
1401
1402
* on the response of that validation.
1402
1403
*
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
+ *
1403
1442
* @property {Array.<Function> } $viewChangeListeners Array of functions to execute whenever the
1404
1443
* view value has changed. It is called with no arguments, and its return value is ignored.
1405
1444
* This can be used in place of additional $watches against the model value.
@@ -1412,6 +1451,7 @@ var VALID_CLASS = 'ng-valid',
1412
1451
* @property {boolean } $dirty True if user has already interacted with the control.
1413
1452
* @property {boolean } $valid True if there is no error.
1414
1453
* @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.
1415
1455
*
1416
1456
* @description
1417
1457
*
@@ -1519,6 +1559,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1519
1559
this . $viewValue = Number . NaN ;
1520
1560
this . $modelValue = Number . NaN ;
1521
1561
this . $validators = { } ;
1562
+ this . $asyncValidators = { } ;
1563
+ this . $validators = { } ;
1522
1564
this . $parsers = [ ] ;
1523
1565
this . $formatters = [ ] ;
1524
1566
this . $viewChangeListeners = [ ] ;
@@ -1586,6 +1628,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1586
1628
1587
1629
var parentForm = $element . inheritedData ( '$formController' ) || nullFormCtrl ,
1588
1630
invalidCount = 0 , // used to easily determine if we are valid
1631
+ pendingCount = 0 , // used to easily determine if there are any pending validations
1589
1632
$error = this . $error = { } ; // keep invalid keys here
1590
1633
1591
1634
@@ -1603,18 +1646,67 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1603
1646
}
1604
1647
1605
1648
this . $$clearValidity = function ( ) {
1649
+ $animate . removeClass ( $element , PENDING_CLASS ) ;
1606
1650
forEach ( ctrl . $error , function ( val , key ) {
1607
1651
var validationKey = snake_case ( key , '-' ) ;
1608
1652
$animate . removeClass ( $element , VALID_CLASS + validationKey ) ;
1609
1653
$animate . removeClass ( $element , INVALID_CLASS + validationKey ) ;
1610
1654
} ) ;
1611
1655
1656
+ // just incase an asnyc validator is still running while
1657
+ // the parser fails
1658
+ if ( ctrl . $pending ) {
1659
+ ctrl . $$clearPending ( ) ;
1660
+ }
1661
+
1612
1662
invalidCount = 0 ;
1613
1663
$error = ctrl . $error = { } ;
1614
1664
1615
1665
parentForm . $$clearControlValidity ( ctrl ) ;
1616
1666
} ;
1617
1667
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
+
1618
1710
/**
1619
1711
* @ngdoc method
1620
1712
* @name ngModel.NgModelController#$setValidity
@@ -1634,28 +1726,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1634
1726
* @param {boolean } isValid Whether the current state is valid (true) or invalid (false).
1635
1727
*/
1636
1728
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
1638
1731
// jshint -W018
1639
- if ( $error [ validationErrorKey ] === ! isValid ) return ;
1732
+ if ( ! ctrl . $pending && $error [ validationErrorKey ] === ! isValid ) return ;
1640
1733
// jshint +W018
1641
1734
1642
1735
if ( isValid ) {
1643
1736
if ( $error [ validationErrorKey ] ) invalidCount -- ;
1644
- if ( ! invalidCount ) {
1737
+ if ( ! invalidCount && ! pendingCount ) {
1645
1738
toggleValidCss ( true ) ;
1646
1739
ctrl . $valid = true ;
1647
1740
ctrl . $invalid = false ;
1648
1741
}
1649
1742
} else if ( ! $error [ validationErrorKey ] ) {
1650
- toggleValidCss ( false ) ;
1651
- ctrl . $invalid = true ;
1652
- ctrl . $valid = false ;
1653
1743
invalidCount ++ ;
1744
+ if ( ! pendingCount ) {
1745
+ toggleValidCss ( false ) ;
1746
+ ctrl . $invalid = true ;
1747
+ ctrl . $valid = false ;
1748
+ }
1654
1749
}
1655
1750
1656
1751
$error [ validationErrorKey ] = ! isValid ;
1657
1752
toggleValidCss ( isValid , validationErrorKey ) ;
1658
-
1659
1753
parentForm . $setValidity ( validationErrorKey , isValid , ctrl ) ;
1660
1754
} ;
1661
1755
@@ -1783,7 +1877,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1783
1877
* @name ngModel.NgModelController#$validate
1784
1878
*
1785
1879
* @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) .
1787
1881
*/
1788
1882
this . $validate = function ( ) {
1789
1883
// ignore $validate before model initialized
@@ -1799,9 +1893,40 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1799
1893
} ;
1800
1894
1801
1895
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 ) ;
1804
1904
} ) ;
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 ) {
1805
1930
ctrl . $modelValue = ctrl . $valid ? modelValue : undefined ;
1806
1931
ctrl . $$invalidModelValue = ctrl . $valid ? undefined : modelValue ;
1807
1932
} ;
@@ -1849,13 +1974,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1849
1974
ctrl . $$invalidModelValue = ctrl . $modelValue = undefined ;
1850
1975
ctrl . $$clearValidity ( ) ;
1851
1976
ctrl . $setValidity ( parserName , false ) ;
1977
+ ctrl . $$writeModelToScope ( ) ;
1852
1978
} else if ( ctrl . $modelValue !== modelValue &&
1853
1979
( isUndefined ( ctrl . $$invalidModelValue ) || ctrl . $$invalidModelValue != modelValue ) ) {
1854
1980
ctrl . $setValidity ( parserName , true ) ;
1855
1981
ctrl . $$runValidators ( modelValue , viewValue ) ;
1982
+ ctrl . $$writeModelToScope ( ) ;
1856
1983
}
1857
-
1858
- ctrl . $$writeModelToScope ( ) ;
1859
1984
} ;
1860
1985
1861
1986
this . $$writeModelToScope = function ( ) {
0 commit comments