mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-07 22:37:28 +08:00
feat(ngModel): provide validation API functions for sync and async validations
This commit introduces a 2nd validation queue called `$asyncValidators`. Each time a value is processed by the validation pipeline, if all synchronous `$validators` succeed, the value is then passed through the `$asyncValidators` validation queue. These validators should return a promise. Rejection of a validation promise indicates a failed validation.
This commit is contained in:
@@ -5,6 +5,7 @@ var nullFormCtrl = {
|
||||
$addControl: noop,
|
||||
$removeControl: noop,
|
||||
$setValidity: noop,
|
||||
$$setPending: noop,
|
||||
$setDirty: noop,
|
||||
$setPristine: noop,
|
||||
$setSubmitted: noop,
|
||||
@@ -54,8 +55,9 @@ function FormController(element, attrs, $scope, $animate) {
|
||||
var form = this,
|
||||
parentForm = element.parent().controller('form') || nullFormCtrl,
|
||||
invalidCount = 0, // used to easily determine if we are valid
|
||||
errors = form.$error = {},
|
||||
controls = [];
|
||||
pendingCount = 0,
|
||||
controls = [],
|
||||
errors = form.$error = {};
|
||||
|
||||
// init state
|
||||
form.$name = attrs.name || attrs.ngForm;
|
||||
@@ -151,9 +153,29 @@ function FormController(element, attrs, $scope, $animate) {
|
||||
};
|
||||
|
||||
form.$$clearControlValidity = function(control) {
|
||||
forEach(errors, function(queue, validationToken) {
|
||||
forEach(form.$pending, clear);
|
||||
forEach(errors, clear);
|
||||
|
||||
function clear(queue, validationToken) {
|
||||
form.$setValidity(validationToken, true, control);
|
||||
});
|
||||
}
|
||||
|
||||
parentForm.$$clearControlValidity(form);
|
||||
};
|
||||
|
||||
form.$$setPending = function(validationToken, control) {
|
||||
var pending = form.$pending && form.$pending[validationToken];
|
||||
|
||||
if (!pending || !includes(pending, control)) {
|
||||
pendingCount++;
|
||||
form.$valid = form.$invalid = undefined;
|
||||
form.$pending = form.$pending || {};
|
||||
if (!pending) {
|
||||
pending = form.$pending[validationToken] = [];
|
||||
}
|
||||
pending.push(control);
|
||||
parentForm.$$setPending(validationToken, form);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -167,24 +189,56 @@ function FormController(element, attrs, $scope, $animate) {
|
||||
*/
|
||||
form.$setValidity = function(validationToken, isValid, control) {
|
||||
var queue = errors[validationToken];
|
||||
var pendingChange, pending = form.$pending && form.$pending[validationToken];
|
||||
|
||||
if (pending) {
|
||||
pendingChange = indexOf(pending, control) >= 0;
|
||||
if (pendingChange) {
|
||||
arrayRemove(pending, control);
|
||||
pendingCount--;
|
||||
|
||||
if (pending.length === 0) {
|
||||
delete form.$pending[validationToken];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pendingNoMore = form.$pending && pendingCount === 0;
|
||||
if (pendingNoMore) {
|
||||
form.$pending = undefined;
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
if (queue) {
|
||||
arrayRemove(queue, control);
|
||||
if (!queue.length) {
|
||||
invalidCount--;
|
||||
if (queue || pendingChange) {
|
||||
if (queue) {
|
||||
arrayRemove(queue, control);
|
||||
}
|
||||
if (!queue || !queue.length) {
|
||||
if (errors[validationToken]) {
|
||||
invalidCount--;
|
||||
}
|
||||
if (!invalidCount) {
|
||||
toggleValidCss(isValid);
|
||||
form.$valid = true;
|
||||
form.$invalid = false;
|
||||
if (!form.$pending) {
|
||||
toggleValidCss(isValid);
|
||||
form.$valid = true;
|
||||
form.$invalid = false;
|
||||
}
|
||||
} else if(pendingNoMore) {
|
||||
toggleValidCss(false);
|
||||
form.$valid = false;
|
||||
form.$invalid = true;
|
||||
}
|
||||
errors[validationToken] = false;
|
||||
toggleValidCss(true, validationToken);
|
||||
parentForm.$setValidity(validationToken, true, form);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if (!form.$pending) {
|
||||
form.$valid = false;
|
||||
form.$invalid = true;
|
||||
}
|
||||
|
||||
if (!invalidCount) {
|
||||
toggleValidCss(isValid);
|
||||
}
|
||||
@@ -197,9 +251,6 @@ function FormController(element, attrs, $scope, $animate) {
|
||||
parentForm.$setValidity(validationToken, false, form);
|
||||
}
|
||||
queue.push(control);
|
||||
|
||||
form.$valid = false;
|
||||
form.$invalid = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1386,7 +1386,8 @@ var VALID_CLASS = 'ng-valid',
|
||||
PRISTINE_CLASS = 'ng-pristine',
|
||||
DIRTY_CLASS = 'ng-dirty',
|
||||
UNTOUCHED_CLASS = 'ng-untouched',
|
||||
TOUCHED_CLASS = 'ng-touched';
|
||||
TOUCHED_CLASS = 'ng-touched',
|
||||
PENDING_CLASS = 'ng-pending';
|
||||
|
||||
/**
|
||||
* @ngdoc type
|
||||
@@ -1421,6 +1422,44 @@ var VALID_CLASS = 'ng-valid',
|
||||
* provided with the model value as an argument and must return a true or false value depending
|
||||
* on the response of that validation.
|
||||
*
|
||||
* ```js
|
||||
* ngModel.$validators.validCharacters = function(modelValue, viewValue) {
|
||||
* var value = modelValue || viewValue;
|
||||
* return /[0-9]+/.test(value) &&
|
||||
* /[a-z]+/.test(value) &&
|
||||
* /[A-Z]+/.test(value) &&
|
||||
* /\W+/.test(value);
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @property {Object.<string, function>} $asyncValidators A collection of validations that are expected to
|
||||
* perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided
|
||||
* is expected to return a promise when it is run during the model validation process. Once the promise
|
||||
* is delivered then the validation status will be set to true when fulfilled and false when rejected.
|
||||
* When the asynchronous validators are trigged, each of the validators will run in parallel and the model
|
||||
* value will only be updated once all validators have been fulfilled. Also, keep in mind that all
|
||||
* asynchronous validators will only run once all synchronous validators have passed.
|
||||
*
|
||||
* Please note that if $http is used then it is important that the server returns a success HTTP response code
|
||||
* in order to fulfill the validation and a status level of `4xx` in order to reject the validation.
|
||||
*
|
||||
* ```js
|
||||
* ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
|
||||
* var value = modelValue || viewValue;
|
||||
* return $http.get('/api/users/' + value).
|
||||
* then(function() {
|
||||
* //username exists, this means the validator fails
|
||||
* return false;
|
||||
* }, function() {
|
||||
* //username does not exist, therefore this validation is true
|
||||
* return true;
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @param {string} name The name of the validator.
|
||||
* @param {Function} validationFn The validation function that will be run.
|
||||
*
|
||||
* @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
|
||||
* view value has changed. It is called with no arguments, and its return value is ignored.
|
||||
* This can be used in place of additional $watches against the model value.
|
||||
@@ -1433,6 +1472,7 @@ var VALID_CLASS = 'ng-valid',
|
||||
* @property {boolean} $dirty True if user has already interacted with the control.
|
||||
* @property {boolean} $valid True if there is no error.
|
||||
* @property {boolean} $invalid True if at least one error on the control.
|
||||
* @property {Object.<string, boolean>} $pending True if one or more asynchronous validators is still yet to be delivered.
|
||||
*
|
||||
* @description
|
||||
*
|
||||
@@ -1540,6 +1580,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
|
||||
this.$viewValue = Number.NaN;
|
||||
this.$modelValue = Number.NaN;
|
||||
this.$validators = {};
|
||||
this.$asyncValidators = {};
|
||||
this.$validators = {};
|
||||
this.$parsers = [];
|
||||
this.$formatters = [];
|
||||
this.$viewChangeListeners = [];
|
||||
@@ -1607,6 +1649,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
|
||||
|
||||
var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
|
||||
invalidCount = 0, // used to easily determine if we are valid
|
||||
pendingCount = 0, // used to easily determine if there are any pending validations
|
||||
$error = this.$error = {}; // keep invalid keys here
|
||||
|
||||
|
||||
@@ -1624,18 +1667,67 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
|
||||
}
|
||||
|
||||
this.$$clearValidity = function() {
|
||||
$animate.removeClass($element, PENDING_CLASS);
|
||||
forEach(ctrl.$error, function(val, key) {
|
||||
var validationKey = snake_case(key, '-');
|
||||
$animate.removeClass($element, VALID_CLASS + validationKey);
|
||||
$animate.removeClass($element, INVALID_CLASS + validationKey);
|
||||
});
|
||||
|
||||
// just incase an asnyc validator is still running while
|
||||
// the parser fails
|
||||
if(ctrl.$pending) {
|
||||
ctrl.$$clearPending();
|
||||
}
|
||||
|
||||
invalidCount = 0;
|
||||
$error = ctrl.$error = {};
|
||||
|
||||
parentForm.$$clearControlValidity(ctrl);
|
||||
};
|
||||
|
||||
this.$$clearPending = function() {
|
||||
pendingCount = 0;
|
||||
ctrl.$pending = undefined;
|
||||
$animate.removeClass($element, PENDING_CLASS);
|
||||
};
|
||||
|
||||
this.$$setPending = function(validationErrorKey, promise, currentValue) {
|
||||
ctrl.$pending = ctrl.$pending || {};
|
||||
if (angular.isUndefined(ctrl.$pending[validationErrorKey])) {
|
||||
ctrl.$pending[validationErrorKey] = true;
|
||||
pendingCount++;
|
||||
}
|
||||
|
||||
ctrl.$valid = ctrl.$invalid = undefined;
|
||||
parentForm.$$setPending(validationErrorKey, ctrl);
|
||||
|
||||
$animate.addClass($element, PENDING_CLASS);
|
||||
$animate.removeClass($element, INVALID_CLASS);
|
||||
$animate.removeClass($element, VALID_CLASS);
|
||||
|
||||
//Special-case for (undefined|null|false|NaN) values to avoid
|
||||
//having to compare each of them with each other
|
||||
currentValue = currentValue || '';
|
||||
promise.then(resolve(true), resolve(false));
|
||||
|
||||
function resolve(bool) {
|
||||
return function() {
|
||||
var value = ctrl.$viewValue || '';
|
||||
if (ctrl.$pending && ctrl.$pending[validationErrorKey] && currentValue === value) {
|
||||
pendingCount--;
|
||||
delete ctrl.$pending[validationErrorKey];
|
||||
ctrl.$setValidity(validationErrorKey, bool);
|
||||
if (pendingCount === 0) {
|
||||
ctrl.$$clearPending();
|
||||
ctrl.$$updateValidModelValue(value);
|
||||
ctrl.$$writeModelToScope();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name ngModel.NgModelController#$setValidity
|
||||
@@ -1655,28 +1747,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
|
||||
* @param {boolean} isValid Whether the current state is valid (true) or invalid (false).
|
||||
*/
|
||||
this.$setValidity = function(validationErrorKey, isValid) {
|
||||
// Purposeful use of ! here to cast isValid to boolean in case it is undefined
|
||||
|
||||
// avoid doing anything if the validation value has not changed
|
||||
// jshint -W018
|
||||
if ($error[validationErrorKey] === !isValid) return;
|
||||
if (!ctrl.$pending && $error[validationErrorKey] === !isValid) return;
|
||||
// jshint +W018
|
||||
|
||||
if (isValid) {
|
||||
if ($error[validationErrorKey]) invalidCount--;
|
||||
if (!invalidCount) {
|
||||
if (!invalidCount && !pendingCount) {
|
||||
toggleValidCss(true);
|
||||
ctrl.$valid = true;
|
||||
ctrl.$invalid = false;
|
||||
}
|
||||
} else if(!$error[validationErrorKey]) {
|
||||
toggleValidCss(false);
|
||||
ctrl.$invalid = true;
|
||||
ctrl.$valid = false;
|
||||
invalidCount++;
|
||||
if (!pendingCount) {
|
||||
toggleValidCss(false);
|
||||
ctrl.$invalid = true;
|
||||
ctrl.$valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
$error[validationErrorKey] = !isValid;
|
||||
toggleValidCss(isValid, validationErrorKey);
|
||||
|
||||
parentForm.$setValidity(validationErrorKey, isValid, ctrl);
|
||||
};
|
||||
|
||||
@@ -1804,7 +1898,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
|
||||
* @name ngModel.NgModelController#$validate
|
||||
*
|
||||
* @description
|
||||
* Runs each of the registered validations set on the $validators object.
|
||||
* Runs each of the registered validators (first synchronous validators and then asynchronous validators).
|
||||
*/
|
||||
this.$validate = function() {
|
||||
// ignore $validate before model initialized
|
||||
@@ -1820,9 +1914,40 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
|
||||
};
|
||||
|
||||
this.$$runValidators = function(modelValue, viewValue) {
|
||||
forEach(ctrl.$validators, function(fn, name) {
|
||||
ctrl.$setValidity(name, fn(modelValue, viewValue));
|
||||
// this is called in the event if incase the input value changes
|
||||
// while a former asynchronous validator is still doing its thing
|
||||
if(ctrl.$pending) {
|
||||
ctrl.$$clearPending();
|
||||
}
|
||||
|
||||
var continueValidation = validate(ctrl.$validators, function(validator, result) {
|
||||
ctrl.$setValidity(validator, result);
|
||||
});
|
||||
|
||||
if (continueValidation) {
|
||||
validate(ctrl.$asyncValidators, function(validator, result) {
|
||||
if (!isPromiseLike(result)) {
|
||||
throw $ngModelMinErr("$asyncValidators",
|
||||
"Expected asynchronous validator to return a promise but got '{0}' instead.", result);
|
||||
}
|
||||
ctrl.$$setPending(validator, result, modelValue);
|
||||
});
|
||||
}
|
||||
|
||||
ctrl.$$updateValidModelValue(modelValue);
|
||||
|
||||
function validate(validators, callback) {
|
||||
var status = true;
|
||||
forEach(validators, function(fn, name) {
|
||||
var result = fn(modelValue, viewValue);
|
||||
callback(name, result);
|
||||
status = status && result;
|
||||
});
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
this.$$updateValidModelValue = function(modelValue) {
|
||||
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
|
||||
ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue;
|
||||
};
|
||||
@@ -1870,13 +1995,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
|
||||
ctrl.$$invalidModelValue = ctrl.$modelValue = undefined;
|
||||
ctrl.$$clearValidity();
|
||||
ctrl.$setValidity(parserName, false);
|
||||
ctrl.$$writeModelToScope();
|
||||
} else if (ctrl.$modelValue !== modelValue &&
|
||||
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
|
||||
ctrl.$setValidity(parserName, true);
|
||||
ctrl.$$runValidators(modelValue, viewValue);
|
||||
ctrl.$$writeModelToScope();
|
||||
}
|
||||
|
||||
ctrl.$$writeModelToScope();
|
||||
};
|
||||
|
||||
this.$$writeModelToScope = function() {
|
||||
|
||||
@@ -579,6 +579,35 @@ describe('form', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('$pending', function() {
|
||||
beforeEach(function() {
|
||||
doc = $compile('<form name="form"></form>')(scope);
|
||||
scope.$digest();
|
||||
});
|
||||
|
||||
it('should set valid and invalid to undefined when a validation error state is set as pending', inject(function($q, $rootScope) {
|
||||
var defer, form = doc.data('$formController');
|
||||
|
||||
var ctrl = {};
|
||||
form.$$setPending('matias', ctrl);
|
||||
|
||||
expect(form.$valid).toBeUndefined();
|
||||
expect(form.$invalid).toBeUndefined();
|
||||
expect(form.$pending.matias).toEqual([ctrl]);
|
||||
|
||||
form.$setValidity('matias', true, ctrl);
|
||||
|
||||
expect(form.$valid).toBe(true);
|
||||
expect(form.$invalid).toBe(false);
|
||||
expect(form.$pending).toBeUndefined();
|
||||
|
||||
form.$setValidity('matias', false, ctrl);
|
||||
|
||||
expect(form.$valid).toBe(false);
|
||||
expect(form.$invalid).toBe(true);
|
||||
expect(form.$pending).toBeUndefined();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('$setPristine', function() {
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('NgModelController', function() {
|
||||
var attrs = {name: 'testAlias', ngModel: 'value'};
|
||||
|
||||
parentFormCtrl = {
|
||||
$$setPending: jasmine.createSpy('$$setPending'),
|
||||
$setValidity: jasmine.createSpy('$setValidity'),
|
||||
$setDirty: jasmine.createSpy('$setDirty'),
|
||||
$$clearControlValidity: noop
|
||||
@@ -377,7 +378,7 @@ describe('NgModelController', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('$validators', function() {
|
||||
describe('validations pipeline', function() {
|
||||
|
||||
it('should perform validations when $validate() is called', function() {
|
||||
ctrl.$validators.uppercase = function(value) {
|
||||
@@ -504,6 +505,251 @@ describe('NgModelController', function() {
|
||||
expect(ctrl.$error.tooLong).toBe(true);
|
||||
expect(ctrl.$error.notNumeric).not.toBe(true);
|
||||
});
|
||||
|
||||
it('should render a validator asynchronously when a promise is returned', inject(function($q) {
|
||||
var defer;
|
||||
ctrl.$asyncValidators.promiseValidator = function(value) {
|
||||
defer = $q.defer();
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
scope.$apply('value = ""');
|
||||
|
||||
expect(ctrl.$valid).toBeUndefined();
|
||||
expect(ctrl.$invalid).toBeUndefined();
|
||||
expect(ctrl.$pending.promiseValidator).toBe(true);
|
||||
|
||||
defer.resolve();
|
||||
scope.$digest();
|
||||
|
||||
expect(ctrl.$valid).toBe(true);
|
||||
expect(ctrl.$invalid).toBe(false);
|
||||
expect(ctrl.$pending).toBeUndefined();
|
||||
|
||||
scope.$apply('value = "123"');
|
||||
|
||||
defer.reject();
|
||||
scope.$digest();
|
||||
|
||||
expect(ctrl.$valid).toBe(false);
|
||||
expect(ctrl.$invalid).toBe(true);
|
||||
expect(ctrl.$pending).toBeUndefined();
|
||||
}));
|
||||
|
||||
it('should throw an error when a promise is not returned for an asynchronous validator', inject(function($q) {
|
||||
ctrl.$asyncValidators.async = function(value) {
|
||||
return true;
|
||||
};
|
||||
|
||||
expect(function() {
|
||||
scope.$apply('value = "123"');
|
||||
}).toThrowMinErr("ngModel", "$asyncValidators",
|
||||
"Expected asynchronous validator to return a promise but got 'true' instead.");
|
||||
}));
|
||||
|
||||
it('should only run the async validators once all the sync validators have passed',
|
||||
inject(function($q) {
|
||||
|
||||
var stages = {};
|
||||
|
||||
stages.sync = { status1 : false, status2: false, count : 0 };
|
||||
ctrl.$validators.syncValidator1 = function(modelValue, viewValue) {
|
||||
stages.sync.count++;
|
||||
return stages.sync.status1;
|
||||
};
|
||||
|
||||
ctrl.$validators.syncValidator2 = function(modelValue, viewValue) {
|
||||
stages.sync.count++;
|
||||
return stages.sync.status2;
|
||||
};
|
||||
|
||||
stages.async = { defer : null, count : 0 };
|
||||
ctrl.$asyncValidators.asyncValidator = function(modelValue, viewValue) {
|
||||
stages.async.defer = $q.defer();
|
||||
stages.async.count++;
|
||||
return stages.async.defer.promise;
|
||||
};
|
||||
|
||||
scope.$apply('value = "123"');
|
||||
|
||||
expect(ctrl.$valid).toBe(false);
|
||||
expect(ctrl.$invalid).toBe(true);
|
||||
|
||||
expect(stages.sync.count).toBe(2);
|
||||
expect(stages.async.count).toBe(0);
|
||||
|
||||
stages.sync.status1 = true;
|
||||
|
||||
scope.$apply('value = "456"');
|
||||
|
||||
expect(stages.sync.count).toBe(4);
|
||||
expect(stages.async.count).toBe(0);
|
||||
|
||||
stages.sync.status2 = true;
|
||||
|
||||
scope.$apply('value = "789"');
|
||||
|
||||
expect(stages.sync.count).toBe(6);
|
||||
expect(stages.async.count).toBe(1);
|
||||
|
||||
stages.async.defer.resolve();
|
||||
scope.$apply();
|
||||
|
||||
expect(ctrl.$valid).toBe(true);
|
||||
expect(ctrl.$invalid).toBe(false);
|
||||
}));
|
||||
|
||||
it('should ignore expired async validation promises once delivered', inject(function($q) {
|
||||
var defer, oldDefer, newDefer;
|
||||
ctrl.$asyncValidators.async = function(value) {
|
||||
defer = $q.defer();
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
scope.$apply('value = ""');
|
||||
oldDefer = defer;
|
||||
scope.$apply('value = "123"');
|
||||
newDefer = defer;
|
||||
|
||||
newDefer.reject();
|
||||
scope.$digest();
|
||||
oldDefer.resolve();
|
||||
scope.$digest();
|
||||
|
||||
expect(ctrl.$valid).toBe(false);
|
||||
expect(ctrl.$invalid).toBe(true);
|
||||
expect(ctrl.$pending).toBeUndefined();
|
||||
}));
|
||||
|
||||
it('should clear and ignore all pending promises when the input values changes', inject(function($q) {
|
||||
var isPending = false;
|
||||
ctrl.$validators.sync = function(value) {
|
||||
isPending = isObject(ctrl.$pending);
|
||||
return true;
|
||||
};
|
||||
|
||||
var defers = [];
|
||||
ctrl.$asyncValidators.async = function(value) {
|
||||
var defer = $q.defer();
|
||||
defers.push(defer);
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
scope.$apply('value = "123"');
|
||||
expect(isPending).toBe(false);
|
||||
expect(ctrl.$valid).toBe(undefined);
|
||||
expect(ctrl.$invalid).toBe(undefined);
|
||||
expect(defers.length).toBe(1);
|
||||
expect(isObject(ctrl.$pending)).toBe(true);
|
||||
|
||||
scope.$apply('value = "456"');
|
||||
expect(isPending).toBe(false);
|
||||
expect(ctrl.$valid).toBe(undefined);
|
||||
expect(ctrl.$invalid).toBe(undefined);
|
||||
expect(defers.length).toBe(2);
|
||||
expect(isObject(ctrl.$pending)).toBe(true);
|
||||
|
||||
defers[1].resolve();
|
||||
scope.$digest();
|
||||
expect(ctrl.$valid).toBe(true);
|
||||
expect(ctrl.$invalid).toBe(false);
|
||||
expect(isObject(ctrl.$pending)).toBe(false);
|
||||
}));
|
||||
|
||||
it('should clear and ignore all pending promises when a parser fails', inject(function($q) {
|
||||
var failParser = false;
|
||||
ctrl.$parsers.push(function(value) {
|
||||
return failParser ? undefined : value;
|
||||
});
|
||||
|
||||
var defer;
|
||||
ctrl.$asyncValidators.async = function(value) {
|
||||
defer = $q.defer();
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
ctrl.$setViewValue('x..y..z');
|
||||
expect(ctrl.$valid).toBe(undefined);
|
||||
expect(ctrl.$invalid).toBe(undefined);
|
||||
|
||||
failParser = true;
|
||||
|
||||
ctrl.$setViewValue('1..2..3');
|
||||
expect(ctrl.$valid).toBe(false);
|
||||
expect(ctrl.$invalid).toBe(true);
|
||||
expect(isObject(ctrl.$pending)).toBe(false);
|
||||
|
||||
defer.resolve();
|
||||
scope.$digest();
|
||||
|
||||
expect(ctrl.$valid).toBe(false);
|
||||
expect(ctrl.$invalid).toBe(true);
|
||||
expect(isObject(ctrl.$pending)).toBe(false);
|
||||
}));
|
||||
|
||||
it('should re-evaluate the form validity state once the asynchronous promise has been delivered',
|
||||
inject(function($compile, $rootScope, $q) {
|
||||
|
||||
var element = $compile('<form name="myForm">' +
|
||||
'<input type="text" name="username" ng-model="username" minlength="10" required />' +
|
||||
'<input type="number" name="age" ng-model="age" min="10" required />' +
|
||||
'</form>')($rootScope);
|
||||
var inputElm = element.find('input');
|
||||
|
||||
var formCtrl = $rootScope.myForm;
|
||||
var usernameCtrl = formCtrl.username;
|
||||
var ageCtrl = formCtrl.age;
|
||||
|
||||
var usernameDefer;
|
||||
usernameCtrl.$asyncValidators.usernameAvailability = function() {
|
||||
usernameDefer = $q.defer();
|
||||
return usernameDefer.promise;
|
||||
};
|
||||
|
||||
$rootScope.$digest();
|
||||
expect(usernameCtrl.$invalid).toBe(true);
|
||||
expect(formCtrl.$invalid).toBe(true);
|
||||
|
||||
usernameCtrl.$setViewValue('valid-username');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(formCtrl.$pending.usernameAvailability).toBeTruthy();
|
||||
expect(usernameCtrl.$invalid).toBe(undefined);
|
||||
expect(formCtrl.$invalid).toBe(undefined);
|
||||
|
||||
usernameDefer.resolve();
|
||||
$rootScope.$digest();
|
||||
expect(usernameCtrl.$invalid).toBe(false);
|
||||
expect(formCtrl.$invalid).toBe(true);
|
||||
|
||||
ageCtrl.$setViewValue(22);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(usernameCtrl.$invalid).toBe(false);
|
||||
expect(ageCtrl.$invalid).toBe(false);
|
||||
expect(formCtrl.$invalid).toBe(false);
|
||||
|
||||
usernameCtrl.$setViewValue('valid');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(usernameCtrl.$invalid).toBe(true);
|
||||
expect(ageCtrl.$invalid).toBe(false);
|
||||
expect(formCtrl.$invalid).toBe(true);
|
||||
|
||||
usernameCtrl.$setViewValue('another-valid-username');
|
||||
$rootScope.$digest();
|
||||
|
||||
usernameDefer.resolve();
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(usernameCtrl.$invalid).toBe(false);
|
||||
expect(formCtrl.$invalid).toBe(false);
|
||||
expect(formCtrl.$pending).toBeFalsy();
|
||||
expect(ageCtrl.$invalid).toBe(false);
|
||||
|
||||
dealoc(element);
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3294,9 +3540,10 @@ describe('NgModel animations', function() {
|
||||
return animations;
|
||||
}
|
||||
|
||||
function assertValidAnimation(animation, event, className) {
|
||||
function assertValidAnimation(animation, event, classNameA, classNameB) {
|
||||
expect(animation.event).toBe(event);
|
||||
expect(animation.args[1]).toBe(className);
|
||||
expect(animation.args[1]).toBe(classNameA);
|
||||
if(classNameB) expect(animation.args[2]).toBe(classNameB);
|
||||
}
|
||||
|
||||
var doc, input, scope, model;
|
||||
|
||||
Reference in New Issue
Block a user