@@ -1386,7 +1386,8 @@ var VALID_CLASS = 'ng-valid',
1386
1386
PRISTINE_CLASS = 'ng-pristine' ,
1387
1387
DIRTY_CLASS = 'ng-dirty' ,
1388
1388
UNTOUCHED_CLASS = 'ng-untouched' ,
1389
- TOUCHED_CLASS = 'ng-touched' ;
1389
+ TOUCHED_CLASS = 'ng-touched' ,
1390
+ PENDING_CLASS = 'ng-pending' ;
1390
1391
1391
1392
/**
1392
1393
* @ngdoc type
@@ -1421,6 +1422,44 @@ var VALID_CLASS = 'ng-valid',
1421
1422
* provided with the model value as an argument and must return a true or false value depending
1422
1423
* on the response of that validation.
1423
1424
*
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
+ *
1424
1463
* @property {Array.<Function> } $viewChangeListeners Array of functions to execute whenever the
1425
1464
* view value has changed. It is called with no arguments, and its return value is ignored.
1426
1465
* This can be used in place of additional $watches against the model value.
@@ -1433,6 +1472,7 @@ var VALID_CLASS = 'ng-valid',
1433
1472
* @property {boolean } $dirty True if user has already interacted with the control.
1434
1473
* @property {boolean } $valid True if there is no error.
1435
1474
* @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.
1436
1476
*
1437
1477
* @description
1438
1478
*
@@ -1540,6 +1580,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1540
1580
this . $viewValue = Number . NaN ;
1541
1581
this . $modelValue = Number . NaN ;
1542
1582
this . $validators = { } ;
1583
+ this . $asyncValidators = { } ;
1584
+ this . $validators = { } ;
1543
1585
this . $parsers = [ ] ;
1544
1586
this . $formatters = [ ] ;
1545
1587
this . $viewChangeListeners = [ ] ;
@@ -1607,6 +1649,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1607
1649
1608
1650
var parentForm = $element . inheritedData ( '$formController' ) || nullFormCtrl ,
1609
1651
invalidCount = 0 , // used to easily determine if we are valid
1652
+ pendingCount = 0 , // used to easily determine if there are any pending validations
1610
1653
$error = this . $error = { } ; // keep invalid keys here
1611
1654
1612
1655
@@ -1624,18 +1667,67 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1624
1667
}
1625
1668
1626
1669
this . $$clearValidity = function ( ) {
1670
+ $animate . removeClass ( $element , PENDING_CLASS ) ;
1627
1671
forEach ( ctrl . $error , function ( val , key ) {
1628
1672
var validationKey = snake_case ( key , '-' ) ;
1629
1673
$animate . removeClass ( $element , VALID_CLASS + validationKey ) ;
1630
1674
$animate . removeClass ( $element , INVALID_CLASS + validationKey ) ;
1631
1675
} ) ;
1632
1676
1677
+ // just incase an asnyc validator is still running while
1678
+ // the parser fails
1679
+ if ( ctrl . $pending ) {
1680
+ ctrl . $$clearPending ( ) ;
1681
+ }
1682
+
1633
1683
invalidCount = 0 ;
1634
1684
$error = ctrl . $error = { } ;
1635
1685
1636
1686
parentForm . $$clearControlValidity ( ctrl ) ;
1637
1687
} ;
1638
1688
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
+
1639
1731
/**
1640
1732
* @ngdoc method
1641
1733
* @name ngModel.NgModelController#$setValidity
@@ -1655,28 +1747,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1655
1747
* @param {boolean } isValid Whether the current state is valid (true) or invalid (false).
1656
1748
*/
1657
1749
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
1659
1752
// jshint -W018
1660
- if ( $error [ validationErrorKey ] === ! isValid ) return ;
1753
+ if ( ! ctrl . $pending && $error [ validationErrorKey ] === ! isValid ) return ;
1661
1754
// jshint +W018
1662
1755
1663
1756
if ( isValid ) {
1664
1757
if ( $error [ validationErrorKey ] ) invalidCount -- ;
1665
- if ( ! invalidCount ) {
1758
+ if ( ! invalidCount && ! pendingCount ) {
1666
1759
toggleValidCss ( true ) ;
1667
1760
ctrl . $valid = true ;
1668
1761
ctrl . $invalid = false ;
1669
1762
}
1670
1763
} else if ( ! $error [ validationErrorKey ] ) {
1671
- toggleValidCss ( false ) ;
1672
- ctrl . $invalid = true ;
1673
- ctrl . $valid = false ;
1674
1764
invalidCount ++ ;
1765
+ if ( ! pendingCount ) {
1766
+ toggleValidCss ( false ) ;
1767
+ ctrl . $invalid = true ;
1768
+ ctrl . $valid = false ;
1769
+ }
1675
1770
}
1676
1771
1677
1772
$error [ validationErrorKey ] = ! isValid ;
1678
1773
toggleValidCss ( isValid , validationErrorKey ) ;
1679
-
1680
1774
parentForm . $setValidity ( validationErrorKey , isValid , ctrl ) ;
1681
1775
} ;
1682
1776
@@ -1804,7 +1898,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1804
1898
* @name ngModel.NgModelController#$validate
1805
1899
*
1806
1900
* @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) .
1808
1902
*/
1809
1903
this . $validate = function ( ) {
1810
1904
// ignore $validate before model initialized
@@ -1820,9 +1914,40 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1820
1914
} ;
1821
1915
1822
1916
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 ) ;
1825
1925
} ) ;
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 ) {
1826
1951
ctrl . $modelValue = ctrl . $valid ? modelValue : undefined ;
1827
1952
ctrl . $$invalidModelValue = ctrl . $valid ? undefined : modelValue ;
1828
1953
} ;
@@ -1870,13 +1995,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
1870
1995
ctrl . $$invalidModelValue = ctrl . $modelValue = undefined ;
1871
1996
ctrl . $$clearValidity ( ) ;
1872
1997
ctrl . $setValidity ( parserName , false ) ;
1998
+ ctrl . $$writeModelToScope ( ) ;
1873
1999
} else if ( ctrl . $modelValue !== modelValue &&
1874
2000
( isUndefined ( ctrl . $$invalidModelValue ) || ctrl . $$invalidModelValue != modelValue ) ) {
1875
2001
ctrl . $setValidity ( parserName , true ) ;
1876
2002
ctrl . $$runValidators ( modelValue , viewValue ) ;
2003
+ ctrl . $$writeModelToScope ( ) ;
1877
2004
}
1878
-
1879
- ctrl . $$writeModelToScope ( ) ;
1880
2005
} ;
1881
2006
1882
2007
this . $$writeModelToScope = function ( ) {
0 commit comments