diff --git a/src/.jshintrc b/src/.jshintrc index f32caa451ed6..5ca02f406eab 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -157,6 +157,9 @@ "INVALID_CLASS": false, "PRISTINE_CLASS": false, "DIRTY_CLASS": false, + "WORKING_CLASS": false, + "IDLE_CLASS": false, + "VALIDATING_CLASS": false, /* ng/directive/form.js */ "nullFormCtrl": false diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 06ffad868d0c..aae421940b46 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -6,7 +6,11 @@ var nullFormCtrl = { $removeControl: noop, $setValidity: noop, $setDirty: noop, - $setPristine: noop + $setPristine: noop, + $setWorking: noop, + $setIdle: noop, + $setValidating: noop, + $clearValidating: noop }; /** @@ -51,7 +55,9 @@ function FormController(element, attrs) { var form = this, parentForm = element.parent().controller('form') || nullFormCtrl, invalidCount = 0, // used to easily determine if we are valid + validatingCount = 0, //used to easily determine if we are validating errors = form.$error = {}, + pendingValidations = form.$pendingValidations = {}, controls = []; // init state @@ -60,11 +66,15 @@ function FormController(element, attrs) { form.$pristine = true; form.$valid = true; form.$invalid = false; + form.$working = false; + form.$idle = true; + form.$validating = false; parentForm.$addControl(form); // Setup initial state of the control element.addClass(PRISTINE_CLASS); + element.addClass(IDLE_CLASS); toggleValidCss(true); // convenience method for easy toggling of classes @@ -94,7 +104,7 @@ function FormController(element, attrs) { if (control.$name) { form[control.$name] = control; } - }; + }; /** * @ngdoc function @@ -113,9 +123,12 @@ function FormController(element, attrs) { forEach(errors, function(queue, validationToken) { form.$setValidity(validationToken, true, control); }); + forEach(pendingValidations, function (queue, validationToken) { + form.$clearValidating(validationToken, control); + }); arrayRemove(controls, control); - }; +}; /** * @ngdoc function @@ -205,7 +218,91 @@ function FormController(element, attrs) { forEach(controls, function(control) { control.$setPristine(); }); - }; + }; + + /** + * @ngdoc function + * @name ng.directive:form.FormController#$setWorking + * @methodOf ng.directive:form.FormController + * + * @description + * Sets the form to the working state + * + */ + form.$setWorking = function () { + element.removeClass(IDLE_CLASS).addClass(WORKING_CLASS); + form.$working = true; + form.$idle = false; + parentForm.$setWorking(); + }; + + /** + * @ngdoc function + * @name ng.directive:form.FormController#$setIdle + * @methodOf ng.directive:form.FormController + * + * @description + * Sets the form to the idle state + * + */ + form.$setIdle = function () { + element.removeClass(WORKING_CLASS).addClass(IDLE_CLASS); + form.$working = false; + form.$idle = true; + parentForm.$setIdle(); + }; + + /** + * @ngdoc function + * @name ng.directive:form.FormController#$setValidating + * @methodOf ng.directive:form.FormController + * + * @description + * Sets the form to the validating state + * + */ + form.$setValidating = function (validationToken, control) { + var queue = pendingValidations[validationToken]; + + if (!validatingCount) { + element.addClass(VALIDATING_CLASS); + } + if (queue) { + if (includes(queue, control)) return; + } else { + pendingValidations[validationToken] = queue = []; + validatingCount++; + parentForm.$setValidating(validationToken, form); + } + queue.push(control); + form.$validating = true; + }; + + /** + * @ngdoc function + * @name ng.directive:form.FormController#$clearValidating + * @methodOf ng.directive:form.FormController + * + * @description + * Clears the form from the validating state + * + */ + form.$clearValidating = function (validationToken, control) { + var queue = pendingValidations[validationToken]; + + if (queue) { + arrayRemove(queue, control); + if (!queue.length) { + validatingCount--; + if (!validatingCount) { + element.removeClass(VALIDATING_CLASS); + form.$validating = false; + } + pendingValidations[validationToken] = false; + parentForm.$clearValidating(validationToken, form); + } + } + }; } @@ -253,6 +350,9 @@ function FormController(element, attrs) { * - `ng-invalid` is set if the form is invalid. * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. + * - `ng-working` is set if the form is working. + * - `ng-idle` is set if the form is idle. + * - `ng-validating` is set if the form is validating. * * * # Submitting a form and preventing the default action @@ -319,61 +419,61 @@ function FormController(element, attrs) { */ -var formDirectiveFactory = function(isNgForm) { - return ['$timeout', function($timeout) { - var formDirective = { - name: 'form', - restrict: isNgForm ? 'EAC' : 'E', - controller: FormController, - compile: function() { - return { - pre: function(scope, formElement, attr, controller) { - if (!attr.action) { - // we can't use jq events because if a form is destroyed during submission the default - // action is not prevented. see #1238 - // - // IE 9 is not affected because it doesn't fire a submit event and try to do a full - // page reload if the form was destroyed by submission of the form via a click handler - // on a button in the form. Looks like an IE9 specific bug. - var preventDefaultListener = function(event) { - event.preventDefault +var formDirectiveFactory = function (isNgForm) { + return ['$timeout', function ($timeout) { + var formDirective = { + name: 'form', + restrict: isNgForm ? 'EAC' : 'E', + controller: FormController, + compile: function () { + return { + pre: function (scope, formElement, attr, controller) { + if (!attr.action) { + // we can't use jq events because if a form is destroyed during submission the default + // action is not prevented. see #1238 + // + // IE 9 is not affected because it doesn't fire a submit event and try to do a full + // page reload if the form was destroyed by submission of the form via a click handler + // on a button in the form. Looks like an IE9 specific bug. + var preventDefaultListener = function (event) { + event.preventDefault ? event.preventDefault() : event.returnValue = false; // IE - }; + }; - addEventListenerFn(formElement[0], 'submit', preventDefaultListener); + addEventListenerFn(formElement[0], 'submit', preventDefaultListener); - // unregister the preventDefault listener so that we don't not leak memory but in a - // way that will achieve the prevention of the default action. - formElement.on('$destroy', function() { - $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); - }, 0, false); - }); - } + // unregister the preventDefault listener so that we don't not leak memory but in a + // way that will achieve the prevention of the default action. + formElement.on('$destroy', function () { + $timeout(function () { + removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); + }, 0, false); + }); + } - var parentFormCtrl = formElement.parent().controller('form'), + var parentFormCtrl = formElement.parent().controller('form'), alias = attr.name || attr.ngForm; - if (alias) { - setter(scope, alias, controller, alias); - } - if (parentFormCtrl) { - formElement.on('$destroy', function() { - parentFormCtrl.$removeControl(controller); - if (alias) { - setter(scope, alias, undefined, alias); - } - extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards - }); + if (alias) { + setter(scope, alias, controller, alias); + } + if (parentFormCtrl) { + formElement.on('$destroy', function () { + parentFormCtrl.$removeControl(controller); + if (alias) { + setter(scope, alias, undefined, alias); + } + extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + }); + } + } + }; } - } }; - } - }; - return formDirective; - }]; + return formDirective; + } ]; }; var formDirective = formDirectiveFactory(); diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 53a8ddd4d70a..c947dd96d5d4 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -5,7 +5,10 @@ -VALID_CLASS, -INVALID_CLASS, -PRISTINE_CLASS, - -DIRTY_CLASS + -DIRTY_CLASS, + -WORKING_CLASS, + -IDLE_CLASS, + -VALIDATING_CLASS */ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; @@ -795,7 +798,10 @@ var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', - DIRTY_CLASS = 'ng-dirty'; + DIRTY_CLASS = 'ng-dirty', + WORKING_CLASS = 'ng-working', + IDLE_CLASS = 'ng-idle', + VALIDATING_CLASS = 'ng-validating'; /** * @ngdoc object @@ -923,210 +929,256 @@ var VALID_CLASS = 'ng-valid', * */ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', - function($scope, $exceptionHandler, $attr, $element, $parse) { - this.$viewValue = Number.NaN; - this.$modelValue = Number.NaN; - this.$parsers = []; - this.$formatters = []; - this.$viewChangeListeners = []; - this.$pristine = true; - this.$dirty = false; - this.$valid = true; - this.$invalid = false; - this.$name = $attr.name; - - var ngModelGet = $parse($attr.ngModel), + function ($scope, $exceptionHandler, $attr, $element, $parse) { + this.$viewValue = Number.NaN; + this.$modelValue = Number.NaN; + this.$parsers = []; + this.$formatters = []; + this.$viewChangeListeners = []; + this.$pristine = true; + this.$dirty = false; + this.$valid = true; + this.$invalid = false; + this.$validating = false; + this.$name = $attr.name; + + var ngModelGet = $parse($attr.ngModel), ngModelSet = ngModelGet.assign; - if (!ngModelSet) { - throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", + if (!ngModelSet) { + throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", $attr.ngModel, startingTag($element)); - } - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$render - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Called when the view needs to be updated. It is expected that the user of the ng-model - * directive will implement this method. - */ - this.$render = noop; + } - /** - * @ngdoc function - * @name { ng.directive:ngModel.NgModelController#$isEmpty - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * This is called when we need to determine if the value of the input is empty. - * - * For instance, the required directive does this to work out if the input has data or not. - * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. - * - * You can override this for input directives whose concept of being empty is different to the - * default. The `checkboxInputType` directive does this because in its case a value of `false` - * implies empty. - */ - this.$isEmpty = function(value) { - return isUndefined(value) || value === '' || value === null || value !== value; - }; + /** + * @ngdoc function + * @name ng.directive:ngModel.NgModelController#$render + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * Called when the view needs to be updated. It is expected that the user of the ng-model + * directive will implement this method. + */ + this.$render = noop; + + /** + * @ngdoc function + * @name { ng.directive:ngModel.NgModelController#$isEmpty + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * This is called when we need to determine if the value of the input is empty. + * + * For instance, the required directive does this to work out if the input has data or not. + * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. + * + * You can override this for input directives whose concept of being empty is different to the + * default. The `checkboxInputType` directive does this because in its case a value of `false` + * implies empty. + */ + this.$isEmpty = function (value) { + return isUndefined(value) || value === '' || value === null || value !== value; + }; - var parentForm = $element.inheritedData('$formController') || nullFormCtrl, + var parentForm = $element.inheritedData('$formController') || nullFormCtrl, invalidCount = 0, // used to easily determine if we are valid $error = this.$error = {}; // keep invalid keys here - // Setup initial state of the control - $element.addClass(PRISTINE_CLASS); - toggleValidCss(true); + // Setup initial state of the control + $element.addClass(PRISTINE_CLASS); + toggleValidCss(true); - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $element. + // convenience method for easy toggling of classes + function toggleValidCss(isValid, validationErrorKey) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + $element. removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } + } - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setValidity - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Change the validity state, and notifies the form when the control changes validity. (i.e. it - * does not notify form if given validator is already marked as invalid). - * - * This method should be called by validators - i.e. the parser or formatter functions. - * - * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. - * The `validationErrorKey` should be in camelCase and will get converted into dash-case - * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` - * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @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 - // jshint -W018 - if ($error[validationErrorKey] === !isValid) return; - // jshint +W018 - - if (isValid) { - if ($error[validationErrorKey]) invalidCount--; - if (!invalidCount) { - toggleValidCss(true); - this.$valid = true; - this.$invalid = false; - } - } else { - toggleValidCss(false); - this.$invalid = true; - this.$valid = false; - invalidCount++; - } + /** + * @ngdoc function + * @name ng.directive:ngModel.NgModelController#$setValidity + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * Change the validity state, and notifies the form when the control changes validity. (i.e. it + * does not notify form if given validator is already marked as invalid). + * + * This method should be called by validators - i.e. the parser or formatter functions. + * + * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign + * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * class and can be bound to as `{{someForm.someControl.$error.myError}}` . + * @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 + // jshint -W018 + if ($error[validationErrorKey] === !isValid) return; + // jshint +W018 + + if (isValid) { + if ($error[validationErrorKey]) invalidCount--; + if (!invalidCount) { + toggleValidCss(true); + this.$valid = true; + this.$invalid = false; + } + } else { + toggleValidCss(false); + this.$invalid = true; + this.$valid = false; + invalidCount++; + } - $error[validationErrorKey] = !isValid; - toggleValidCss(isValid, validationErrorKey); + $error[validationErrorKey] = !isValid; + toggleValidCss(isValid, validationErrorKey); - parentForm.$setValidity(validationErrorKey, isValid, this); - }; + parentForm.$setValidity(validationErrorKey, isValid, this); + }; - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setPristine - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Sets the control to its pristine state. - * - * This method can be called to remove the 'ng-dirty' class and set the control to its pristine - * state (ng-pristine class). - */ - this.$setPristine = function () { - this.$dirty = false; - this.$pristine = true; - $element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); - }; + /** + * @ngdoc function + * @name ng.directive:ngModel.NgModelController#$setValidating + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * Change the validating state, and notifies the form when the control changes to the validating state. + * + * This method should be called by validators that perform async validations + * + * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign + * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * class and can be bound to as `{{someForm.someControl.$error.myError}}` . + */ + this.$setValidating = function (validationErrorKey) { + $element.addClass(VALIDATING_CLASS); + this.$validating = true; + parentForm.$setValidating(validationErrorKey, this); + }; - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setViewValue - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Update the view value. - * - * This method should be called when the view value changes, typically from within a DOM event handler. - * For example {@link ng.directive:input input} and - * {@link ng.directive:select select} directives call it. - * - * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, - * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to - * `$modelValue` and the **expression** specified in the `ng-model` attribute. - * - * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. - * - * Note that calling this function does not trigger a `$digest`. - * - * @param {string} value Value from the view. - */ - this.$setViewValue = function(value) { - this.$viewValue = value; - - // change to dirty - if (this.$pristine) { - this.$dirty = true; - this.$pristine = false; - $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); - parentForm.$setDirty(); - } + /** + * @ngdoc function + * @name ng.directive:ngModel.NgModelController#$clearValidating + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * Clears the validating state, and notifies the form when the control changes from the validating state. + * + * This method should be called by validators that perform async validations + * + * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign + * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * class and can be bound to as `{{someForm.someControl.$error.myError}}` . + */ + this.$clearValidating = function (validationErrorKey) { + $element.removeClass(VALIDATING_CLASS); + this.$validating = false; + parentForm.$clearValidating(validationErrorKey, this); + }; - forEach(this.$parsers, function(fn) { - value = fn(value); - }); - if (this.$modelValue !== value) { - this.$modelValue = value; - ngModelSet($scope, value); - forEach(this.$viewChangeListeners, function(listener) { - try { - listener(); - } catch(e) { - $exceptionHandler(e); - } - }); - } - }; + /** + * @ngdoc function + * @name ng.directive:ngModel.NgModelController#$setPristine + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the 'ng-dirty' class and set the control to its pristine + * state (ng-pristine class). + */ + this.$setPristine = function () { + this.$dirty = false; + this.$pristine = true; + $element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); + }; + + /** + * @ngdoc function + * @name ng.directive:ngModel.NgModelController#$setViewValue + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * Update the view value. + * + * This method should be called when the view value changes, typically from within a DOM event handler. + * For example {@link ng.directive:input input} and + * {@link ng.directive:select select} directives call it. + * + * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, + * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to + * `$modelValue` and the **expression** specified in the `ng-model` attribute. + * + * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. + * + * Note that calling this function does not trigger a `$digest`. + * + * @param {string} value Value from the view. + */ + this.$setViewValue = function (value) { + this.$viewValue = value; + + // change to dirty + if (this.$pristine) { + this.$dirty = true; + this.$pristine = false; + $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); + parentForm.$setDirty(); + } + + forEach(this.$parsers, function (fn) { + value = fn(value); + }); + + if (this.$modelValue !== value) { + this.$modelValue = value; + ngModelSet($scope, value); + forEach(this.$viewChangeListeners, function (listener) { + try { + listener(); + } catch (e) { + $exceptionHandler(e); + } + }); + } + }; - // model -> value - var ctrl = this; + // model -> value + var ctrl = this; - $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); + $scope.$watch(function ngModelWatch() { + var value = ngModelGet($scope); - // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { + // if scope model value and ngModel value are out of sync + if (ctrl.$modelValue !== value) { - var formatters = ctrl.$formatters, + var formatters = ctrl.$formatters, idx = formatters.length; - ctrl.$modelValue = value; - while(idx--) { - value = formatters[idx](value); - } + ctrl.$modelValue = value; + while (idx--) { + value = formatters[idx](value); + } - if (ctrl.$viewValue !== value) { - ctrl.$viewValue = value; - ctrl.$render(); - } - } + if (ctrl.$viewValue !== value) { + ctrl.$viewValue = value; + ctrl.$render(); + } + } - return value; - }); -}]; + return value; + }); + } ]; /** diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js index c5d7d6cfd3c6..8684b6b81204 100644 --- a/test/helpers/matchers.js +++ b/test/helpers/matchers.js @@ -46,6 +46,8 @@ beforeEach(function() { toBeValid: cssMatcher('ng-valid', 'ng-invalid'), toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'), toBePristine: cssMatcher('ng-pristine', 'ng-dirty'), + toBeWorking: cssMatcher('ng-working', 'ng-idle'), + toBeIdle: cssMatcher('ng-idle', 'ng-working'), toBeShown: function() { this.message = valueFn( "Expected element " + (this.isNot ? "": "not ") + "to have 'ng-hide' class"); diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index dde6f0a026c8..47956b5f528b 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -1,103 +1,105 @@ 'use strict'; -describe('form', function() { - var doc, control, scope, $compile, changeInputValue; - - beforeEach(module(function($compileProvider) { - $compileProvider.directive('storeModelCtrl', function() { - return { - require: 'ngModel', - link: function(scope, elm, attr, ctrl) { - control = ctrl; - } - }; - }); - })); +describe('form', function () { + var doc, control, scope, $compile, changeInputValue; + + beforeEach(module(function ($compileProvider) { + $compileProvider.directive('storeModelCtrl', function () { + return { + require: 'ngModel', + link: function (scope, elm, attr, ctrl) { + control = ctrl; + } + }; + }); + })); - beforeEach(inject(function($injector, $sniffer) { - $compile = $injector.get('$compile'); - scope = $injector.get('$rootScope'); + beforeEach(inject(function ($injector, $sniffer) { + $compile = $injector.get('$compile'); + scope = $injector.get('$rootScope'); - changeInputValue = function(elm, value) { - elm.val(value); - browserTrigger(elm, $sniffer.hasEvent('input') ? 'input' : 'change'); - }; - })); + changeInputValue = function (elm, value) { + elm.val(value); + browserTrigger(elm, $sniffer.hasEvent('input') ? 'input' : 'change'); + }; + })); - afterEach(function() { - dealoc(doc); - }); + afterEach(function () { + dealoc(doc); + }); - it('should instantiate form and attach it to DOM', function() { - doc = $compile('
')(scope); - expect(doc.data('$formController')).toBeTruthy(); - expect(doc.data('$formController') instanceof FormController).toBe(true); - }); + it('should instantiate form and attach it to DOM', function () { + doc = $compile('')(scope); + expect(doc.data('$formController')).toBeTruthy(); + expect(doc.data('$formController') instanceof FormController).toBe(true); + }); - it('should remove form control references from the form when nested control is removed from the DOM', function() { - doc = $compile( + it('should remove form control references from the form when nested control is removed from the DOM', function () { + doc = $compile( '' + '' + '
')(scope); - scope.inputPresent = true; - scope.$digest(); - - var form = scope.myForm; - control.$setValidity('required', false); - expect(form.alias).toBe(control); - expect(form.$error.required).toEqual([control]); - - // remove nested control - scope.inputPresent = false; - scope.$apply(); - - expect(form.$error.required).toBe(false); - expect(form.alias).toBeUndefined(); - }); - + scope.inputPresent = true; + scope.$digest(); + + var form = scope.myForm; + control.$setValidity('required', false); + control.$setValidating('asyncValidation'); + expect(form.alias).toBe(control); + expect(form.$error.required).toEqual([control]); + expect(form.$validating).toBe(true); + + // remove nested control + scope.inputPresent = false; + scope.$apply(); + + expect(form.$validating).toBe(false); + expect(form.$error.required).toBe(false); + expect(form.alias).toBeUndefined(); + }); - it('should use ngForm value as form name', function() { - doc = $compile( + it('should use ngForm value as form name', function () { + doc = $compile( '
' + '' + '
')(scope); - expect(scope.myForm).toBeDefined(); - expect(scope.myForm.alias).toBeDefined(); - }); + expect(scope.myForm).toBeDefined(); + expect(scope.myForm.alias).toBeDefined(); + }); - it('should use ngForm value as form name when nested inside form', function () { - doc = $compile( + it('should use ngForm value as form name when nested inside form', function () { + doc = $compile( '
' + '
' + '
')(scope); - expect(scope.myForm).toBeDefined(); - expect(scope.myForm.nestedForm).toBeDefined(); - expect(scope.myForm.nestedForm.alias).toBeDefined(); - }); + expect(scope.myForm).toBeDefined(); + expect(scope.myForm.nestedForm).toBeDefined(); + expect(scope.myForm.nestedForm.alias).toBeDefined(); + }); - it('should publish form to scope when name attr is defined', function() { - doc = $compile('
')(scope); - expect(scope.myForm).toBeTruthy(); - expect(doc.data('$formController')).toBeTruthy(); - expect(doc.data('$formController')).toEqual(scope.myForm); - }); + it('should publish form to scope when name attr is defined', function () { + doc = $compile('
')(scope); + expect(scope.myForm).toBeTruthy(); + expect(doc.data('$formController')).toBeTruthy(); + expect(doc.data('$formController')).toEqual(scope.myForm); + }); - it('should support expression in form name', function() { - doc = $compile('
')(scope); + it('should support expression in form name', function () { + doc = $compile('
')(scope); - expect(scope.obj).toBeDefined(); - expect(scope.obj.myForm).toBeTruthy(); - }); + expect(scope.obj).toBeDefined(); + expect(scope.obj.myForm).toBeTruthy(); + }); - it('should support two forms on a single scope', function() { - doc = $compile( + it('should support two forms on a single scope', function () { + doc = $compile( '
' + '
' + '' + @@ -108,489 +110,601 @@ describe('form', function() { '
' )(scope); - scope.$apply(); + scope.$apply(); - expect(scope.formA.$error.required.length).toBe(1); - expect(scope.formA.$error.required).toEqual([scope.formA.firstName]); - expect(scope.formB.$error.required.length).toBe(1); - expect(scope.formB.$error.required).toEqual([scope.formB.lastName]); + expect(scope.formA.$error.required.length).toBe(1); + expect(scope.formA.$error.required).toEqual([scope.formA.firstName]); + expect(scope.formB.$error.required.length).toBe(1); + expect(scope.formB.$error.required).toEqual([scope.formB.lastName]); - var inputA = doc.find('input').eq(0), + var inputA = doc.find('input').eq(0), inputB = doc.find('input').eq(1); - changeInputValue(inputA, 'val1'); - changeInputValue(inputB, 'val2'); + changeInputValue(inputA, 'val1'); + changeInputValue(inputB, 'val2'); - expect(scope.firstName).toBe('val1'); - expect(scope.lastName).toBe('val2'); + expect(scope.firstName).toBe('val1'); + expect(scope.lastName).toBe('val2'); - expect(scope.formA.$error.required).toBe(false); - expect(scope.formB.$error.required).toBe(false); - }); + expect(scope.formA.$error.required).toBe(false); + expect(scope.formB.$error.required).toBe(false); + }); - it('should publish widgets', function() { - doc = jqLite(''); - $compile(doc)(scope); + it('should publish widgets', function () { + doc = jqLite('
'); + $compile(doc)(scope); - var widget = scope.form.w1; - expect(widget).toBeDefined(); - expect(widget.$pristine).toBe(true); - expect(widget.$dirty).toBe(false); - expect(widget.$valid).toBe(true); - expect(widget.$invalid).toBe(false); - }); + var widget = scope.form.w1; + expect(widget).toBeDefined(); + expect(widget.$pristine).toBe(true); + expect(widget.$dirty).toBe(false); + expect(widget.$valid).toBe(true); + expect(widget.$invalid).toBe(false); + expect(widget.$validating).toBe(false); + }); - it('should throw an exception if an input has name="hasOwnProperty"', function() { - doc = jqLite( - '
'+ - ''+ - ''+ + it('should throw an exception if an input has name="hasOwnProperty"', function () { + doc = jqLite( + '' + + '' + + '' + '
'); - expect(function() { - $compile(doc)(scope); - }).toThrowMinErr('ng', 'badname'); - }); + expect(function () { + $compile(doc)(scope); + }).toThrowMinErr('ng', 'badname'); + }); - describe('preventing default submission', function() { + describe('preventing default submission', function () { - it('should prevent form submission', function() { - var nextTurn = false, + it('should prevent form submission', function () { + var nextTurn = false, submitted = false, reloadPrevented; - doc = jqLite('
' + + doc = jqLite('' + '' + '
'); - var assertPreventDefaultListener = function(e) { - reloadPrevented = e.defaultPrevented || (e.returnValue === false); - }; + var assertPreventDefaultListener = function (e) { + reloadPrevented = e.defaultPrevented || (e.returnValue === false); + }; - // native dom event listeners in IE8 fire in LIFO order so we have to register them - // there in different order than in other browsers - if (msie==8) addEventListenerFn(doc[0], 'submit', assertPreventDefaultListener); + // native dom event listeners in IE8 fire in LIFO order so we have to register them + // there in different order than in other browsers + if (msie == 8) addEventListenerFn(doc[0], 'submit', assertPreventDefaultListener); - $compile(doc)(scope); + $compile(doc)(scope); - scope.submitMe = function() { - submitted = true; - } + scope.submitMe = function () { + submitted = true; + } - if (msie!=8) addEventListenerFn(doc[0], 'submit', assertPreventDefaultListener); + if (msie != 8) addEventListenerFn(doc[0], 'submit', assertPreventDefaultListener); - browserTrigger(doc.find('input')); + browserTrigger(doc.find('input')); - // let the browser process all events (and potentially reload the page) - setTimeout(function() { nextTurn = true;}); + // let the browser process all events (and potentially reload the page) + setTimeout(function () { nextTurn = true; }); - waitsFor(function() { return nextTurn; }); + waitsFor(function () { return nextTurn; }); - runs(function() { - expect(reloadPrevented).toBe(true); - expect(submitted).toBe(true); + runs(function () { + expect(reloadPrevented).toBe(true); + expect(submitted).toBe(true); - // prevent mem leak in test - removeEventListenerFn(doc[0], 'submit', assertPreventDefaultListener); - }); - }); + // prevent mem leak in test + removeEventListenerFn(doc[0], 'submit', assertPreventDefaultListener); + }); + }); - it('should prevent the default when the form is destroyed by a submission via a click event', - inject(function($timeout) { - doc = jqLite('
' + + it('should prevent the default when the form is destroyed by a submission via a click event', + inject(function ($timeout) { + doc = jqLite('
' + '
' + '' + '
' + '
'); - var form = doc.find('form'), + var form = doc.find('form'), destroyed = false, nextTurn = false, submitted = false, reloadPrevented; - scope.destroy = function() { - // yes, I know, scope methods should not do direct DOM manipulation, but I wanted to keep - // this test small. Imagine that the destroy action will cause a model change (e.g. - // $location change) that will cause some directive to destroy the dom (e.g. ngView+$route) - doc.empty(); - destroyed = true; - } + scope.destroy = function () { + // yes, I know, scope methods should not do direct DOM manipulation, but I wanted to keep + // this test small. Imagine that the destroy action will cause a model change (e.g. + // $location change) that will cause some directive to destroy the dom (e.g. ngView+$route) + doc.empty(); + destroyed = true; + } - scope.submitMe = function() { - submitted = true; - } + scope.submitMe = function () { + submitted = true; + } - var assertPreventDefaultListener = function(e) { - reloadPrevented = e.defaultPrevented || (e.returnValue === false); - }; + var assertPreventDefaultListener = function (e) { + reloadPrevented = e.defaultPrevented || (e.returnValue === false); + }; - // native dom event listeners in IE8 fire in LIFO order so we have to register them - // there in different order than in other browsers - if (msie == 8) addEventListenerFn(form[0], 'submit', assertPreventDefaultListener); + // native dom event listeners in IE8 fire in LIFO order so we have to register them + // there in different order than in other browsers + if (msie == 8) addEventListenerFn(form[0], 'submit', assertPreventDefaultListener); - $compile(doc)(scope); + $compile(doc)(scope); - if (msie != 8) addEventListenerFn(form[0], 'submit', assertPreventDefaultListener); + if (msie != 8) addEventListenerFn(form[0], 'submit', assertPreventDefaultListener); - browserTrigger(doc.find('button'), 'click'); + browserTrigger(doc.find('button'), 'click'); - // let the browser process all events (and potentially reload the page) - setTimeout(function() { nextTurn = true;}, 100); + // let the browser process all events (and potentially reload the page) + setTimeout(function () { nextTurn = true; }, 100); - waitsFor(function() { return nextTurn; }); + waitsFor(function () { return nextTurn; }); - // I can't get IE8 to automatically trigger submit in this test, in production it does it - // properly - if (msie == 8) browserTrigger(form, 'submit'); + // I can't get IE8 to automatically trigger submit in this test, in production it does it + // properly + if (msie == 8) browserTrigger(form, 'submit'); - runs(function() { - expect(doc.html()).toBe(''); - expect(destroyed).toBe(true); - expect(submitted).toBe(false); // this is known corner-case that is not currently handled - // the issue is that the submit listener is destroyed before - // the event propagates there. we can fix this if we see - // the issue in the wild, I'm not going to bother to do it - // now. (i) + runs(function () { + expect(doc.html()).toBe(''); + expect(destroyed).toBe(true); + expect(submitted).toBe(false); // this is known corner-case that is not currently handled + // the issue is that the submit listener is destroyed before + // the event propagates there. we can fix this if we see + // the issue in the wild, I'm not going to bother to do it + // now. (i) - // IE9 and IE10 are special and don't fire submit event when form was destroyed - if (msie < 9) { - expect(reloadPrevented).toBe(true); - $timeout.flush(); - } + // IE9 and IE10 are special and don't fire submit event when form was destroyed + if (msie < 9) { + expect(reloadPrevented).toBe(true); + $timeout.flush(); + } - // prevent mem leak in test - removeEventListenerFn(form[0], 'submit', assertPreventDefaultListener); - }); - })); + // prevent mem leak in test + removeEventListenerFn(form[0], 'submit', assertPreventDefaultListener); + }); + })); - it('should NOT prevent form submission if action attribute present', function() { - var callback = jasmine.createSpy('submit').andCallFake(function(event) { - expect(event.isDefaultPrevented()).toBe(false); - event.preventDefault(); - }); + it('should NOT prevent form submission if action attribute present', function () { + var callback = jasmine.createSpy('submit').andCallFake(function (event) { + expect(event.isDefaultPrevented()).toBe(false); + event.preventDefault(); + }); - doc = $compile('
')(scope); - doc.on('submit', callback); + doc = $compile('
')(scope); + doc.on('submit', callback); - browserTrigger(doc, 'submit'); - expect(callback).toHaveBeenCalledOnce(); + browserTrigger(doc, 'submit'); + expect(callback).toHaveBeenCalledOnce(); + }); }); - }); - describe('nested forms', function() { + describe('nested forms', function () { - it('should chain nested forms', function() { - doc = jqLite( + it('should chain nested forms', function () { + doc = jqLite( '' + '' + '' + '' + '' + ''); - $compile(doc)(scope); + $compile(doc)(scope); - var parent = scope.parent, + var parent = scope.parent, child = scope.child, inputA = child.inputA, inputB = child.inputB; - inputA.$setValidity('MyError', false); - inputB.$setValidity('MyError', false); - expect(parent.$error.MyError).toEqual([child]); - expect(child.$error.MyError).toEqual([inputA, inputB]); + inputA.$setValidity('MyError', false); + inputB.$setValidity('MyError', false); + expect(parent.$error.MyError).toEqual([child]); + expect(child.$error.MyError).toEqual([inputA, inputB]); - inputA.$setValidity('MyError', true); - expect(parent.$error.MyError).toEqual([child]); - expect(child.$error.MyError).toEqual([inputB]); + inputA.$setValidity('MyError', true); + expect(parent.$error.MyError).toEqual([child]); + expect(child.$error.MyError).toEqual([inputB]); - inputB.$setValidity('MyError', true); - expect(parent.$error.MyError).toBe(false); - expect(child.$error.MyError).toBe(false); + inputB.$setValidity('MyError', true); + expect(parent.$error.MyError).toBe(false); + expect(child.$error.MyError).toBe(false); - child.$setDirty(); - expect(parent.$dirty).toBeTruthy(); - }); + child.$setDirty(); + expect(parent.$dirty).toBeTruthy(); + + child.$setWorking(); + expect(parent.$working).toBeTruthy(); + expect(parent.$idle).toBe(false); + + child.$setIdle(); + expect(parent.$working).toBe(false); + expect(parent.$idle).toBeTruthy(); + + child.$setValidating(); + expect(parent.$validating).toBeTruthy(); + + child.$clearValidating(); + expect(parent.$validating).toBe(false); + }); - it('should deregister a child form when its DOM is removed', function() { - doc = jqLite( + it('should deregister a child form when its DOM is removed', function () { + doc = jqLite( '
' + '
' + '' + '
' + '
'); - $compile(doc)(scope); - scope.$apply(); + $compile(doc)(scope); + scope.$apply(); - var parent = scope.parent, + var parent = scope.parent, child = scope.child; - expect(parent).toBeDefined(); - expect(child).toBeDefined(); - expect(parent.$error.required).toEqual([child]); - doc.children().remove(); //remove child + expect(parent).toBeDefined(); + expect(child).toBeDefined(); + expect(parent.$error.required).toEqual([child]); + doc.children().remove(); //remove child - expect(parent.child).toBeUndefined(); - expect(scope.child).toBeUndefined(); - expect(parent.$error.required).toBe(false); - }); + expect(parent.child).toBeUndefined(); + expect(scope.child).toBeUndefined(); + expect(parent.$error.required).toBe(false); + }); - it('should deregister a child form whose name is an expression when its DOM is removed', function() { - doc = jqLite( + it('should deregister a child form whose name is an expression when its DOM is removed', function () { + doc = jqLite( '
' + '
' + '' + '
' + '
'); - $compile(doc)(scope); - scope.$apply(); + $compile(doc)(scope); + scope.$apply(); - var parent = scope.parent, + var parent = scope.parent, child = scope.child.form; - expect(parent).toBeDefined(); - expect(child).toBeDefined(); - expect(parent.$error.required).toEqual([child]); - doc.children().remove(); //remove child + expect(parent).toBeDefined(); + expect(child).toBeDefined(); + expect(parent.$error.required).toEqual([child]); + doc.children().remove(); //remove child - expect(parent.child).toBeUndefined(); - expect(scope.child.form).toBeUndefined(); - expect(parent.$error.required).toBe(false); - }); + expect(parent.child).toBeUndefined(); + expect(scope.child.form).toBeUndefined(); + expect(parent.$error.required).toBe(false); + }); - it('should deregister a input when it is removed from DOM', function() { - doc = jqLite( + it('should deregister a input when it is removed from DOM', function () { + doc = jqLite( '
' + '
' + '' + '
' + '
'); - $compile(doc)(scope); - scope.inputPresent = true; - scope.$apply(); + $compile(doc)(scope); + scope.inputPresent = true; + scope.$apply(); - var parent = scope.parent, + var parent = scope.parent, child = scope.child, input = child.inputA; - expect(parent).toBeDefined(); - expect(child).toBeDefined(); - expect(parent.$error.required).toEqual([child]); - expect(child.$error.required).toEqual([input]); - expect(doc.hasClass('ng-invalid')).toBe(true); - expect(doc.hasClass('ng-invalid-required')).toBe(true); - expect(doc.find('div').hasClass('ng-invalid')).toBe(true); - expect(doc.find('div').hasClass('ng-invalid-required')).toBe(true); - - //remove child input - scope.inputPresent = false; - scope.$apply(); - - expect(parent.$error.required).toBe(false); - expect(child.$error.required).toBe(false); - expect(doc.hasClass('ng-valid')).toBe(true); - expect(doc.hasClass('ng-valid-required')).toBe(true); - expect(doc.find('div').hasClass('ng-valid')).toBe(true); - expect(doc.find('div').hasClass('ng-valid-required')).toBe(true); - }); - - - it('should chain nested forms in repeater', function() { - doc = jqLite( + expect(parent).toBeDefined(); + expect(child).toBeDefined(); + expect(parent.$error.required).toEqual([child]); + expect(child.$error.required).toEqual([input]); + expect(doc.hasClass('ng-invalid')).toBe(true); + expect(doc.hasClass('ng-invalid-required')).toBe(true); + expect(doc.find('div').hasClass('ng-invalid')).toBe(true); + expect(doc.find('div').hasClass('ng-invalid-required')).toBe(true); + + //remove child input + scope.inputPresent = false; + scope.$apply(); + + expect(parent.$error.required).toBe(false); + expect(child.$error.required).toBe(false); + expect(doc.hasClass('ng-valid')).toBe(true); + expect(doc.hasClass('ng-valid-required')).toBe(true); + expect(doc.find('div').hasClass('ng-valid')).toBe(true); + expect(doc.find('div').hasClass('ng-valid-required')).toBe(true); + }); + + + it('should chain nested forms in repeater', function () { + doc = jqLite( '' + '' + '' + '' + ''); - $compile(doc)(scope); + $compile(doc)(scope); - scope.$apply(function() { - scope.forms = [1]; - }); + scope.$apply(function () { + scope.forms = [1]; + }); - var parent = scope.parent; - var child = doc.find('input').scope().child; - var input = child.text; + var parent = scope.parent; + var child = doc.find('input').scope().child; + var input = child.text; - expect(parent).toBeDefined(); - expect(child).toBeDefined(); - expect(input).toBeDefined(); + expect(parent).toBeDefined(); + expect(child).toBeDefined(); + expect(input).toBeDefined(); - input.$setValidity('myRule', false); - expect(input.$error.myRule).toEqual(true); - expect(child.$error.myRule).toEqual([input]); - expect(parent.$error.myRule).toEqual([child]); + input.$setValidity('myRule', false); + expect(input.$error.myRule).toEqual(true); + expect(child.$error.myRule).toEqual([input]); + expect(parent.$error.myRule).toEqual([child]); - input.$setValidity('myRule', true); - expect(parent.$error.myRule).toBe(false); - expect(child.$error.myRule).toBe(false); - }); - }) + input.$setValidity('myRule', true); + expect(parent.$error.myRule).toBe(false); + expect(child.$error.myRule).toBe(false); + }); + }) - describe('validation', function() { + describe('validation', function () { - beforeEach(function() { - doc = $compile( + beforeEach(function () { + doc = $compile( '
' + '' + '
')(scope); - scope.$digest(); - }); + scope.$digest(); + }); - it('should have ng-valid/ng-invalid css class', function() { - expect(doc).toBeValid(); - - control.$setValidity('error', false); - expect(doc).toBeInvalid(); - expect(doc.hasClass('ng-valid-error')).toBe(false); - expect(doc.hasClass('ng-invalid-error')).toBe(true); - - control.$setValidity('another', false); - expect(doc.hasClass('ng-valid-error')).toBe(false); - expect(doc.hasClass('ng-invalid-error')).toBe(true); - expect(doc.hasClass('ng-valid-another')).toBe(false); - expect(doc.hasClass('ng-invalid-another')).toBe(true); - - control.$setValidity('error', true); - expect(doc).toBeInvalid(); - expect(doc.hasClass('ng-valid-error')).toBe(true); - expect(doc.hasClass('ng-invalid-error')).toBe(false); - expect(doc.hasClass('ng-valid-another')).toBe(false); - expect(doc.hasClass('ng-invalid-another')).toBe(true); - - control.$setValidity('another', true); - expect(doc).toBeValid(); - expect(doc.hasClass('ng-valid-error')).toBe(true); - expect(doc.hasClass('ng-invalid-error')).toBe(false); - expect(doc.hasClass('ng-valid-another')).toBe(true); - expect(doc.hasClass('ng-invalid-another')).toBe(false); - }); + it('should have ng-valid/ng-invalid css class', function () { + expect(doc).toBeValid(); + + control.$setValidity('error', false); + expect(doc).toBeInvalid(); + expect(doc.hasClass('ng-valid-error')).toBe(false); + expect(doc.hasClass('ng-invalid-error')).toBe(true); + + control.$setValidity('another', false); + expect(doc.hasClass('ng-valid-error')).toBe(false); + expect(doc.hasClass('ng-invalid-error')).toBe(true); + expect(doc.hasClass('ng-valid-another')).toBe(false); + expect(doc.hasClass('ng-invalid-another')).toBe(true); + + control.$setValidity('error', true); + expect(doc).toBeInvalid(); + expect(doc.hasClass('ng-valid-error')).toBe(true); + expect(doc.hasClass('ng-invalid-error')).toBe(false); + expect(doc.hasClass('ng-valid-another')).toBe(false); + expect(doc.hasClass('ng-invalid-another')).toBe(true); + + control.$setValidity('another', true); + expect(doc).toBeValid(); + expect(doc.hasClass('ng-valid-error')).toBe(true); + expect(doc.hasClass('ng-invalid-error')).toBe(false); + expect(doc.hasClass('ng-valid-another')).toBe(true); + expect(doc.hasClass('ng-invalid-another')).toBe(false); + control.$setValidating('async'); + expect(doc.hasClass('ng-validating')).toBe(true); + control.$clearValidating('async'); + expect(doc.hasClass('ng-validating')).toBe(false); + }); - it('should have ng-pristine/ng-dirty css class', function() { - expect(doc).toBePristine(); - control.$setViewValue(''); - scope.$apply(); - expect(doc).toBeDirty(); + it('should have ng-pristine/ng-dirty css class', function () { + expect(doc).toBePristine(); + + control.$setViewValue(''); + scope.$apply(); + expect(doc).toBeDirty(); + }); }); - }); - describe('$setPristine', function() { + describe('$setPristine', function () { - it('should reset pristine state of form and controls', function() { + it('should reset pristine state of form and controls', function () { - doc = $compile( + doc = $compile( '
' + '' + '' + '
')(scope); - scope.$digest(); + scope.$digest(); - var form = doc, + var form = doc, formCtrl = scope.testForm, input1 = form.find('input').eq(0), input1Ctrl = input1.controller('ngModel'), input2 = form.find('input').eq(1), input2Ctrl = input2.controller('ngModel'); - input1Ctrl.$setViewValue('xx'); - input2Ctrl.$setViewValue('yy'); - scope.$apply(); - expect(form).toBeDirty(); - expect(input1).toBeDirty(); - expect(input2).toBeDirty(); - - formCtrl.$setPristine(); - expect(form).toBePristine(); - expect(formCtrl.$pristine).toBe(true); - expect(formCtrl.$dirty).toBe(false); - expect(input1).toBePristine(); - expect(input1Ctrl.$pristine).toBe(true); - expect(input1Ctrl.$dirty).toBe(false); - expect(input2).toBePristine(); - expect(input2Ctrl.$pristine).toBe(true); - expect(input2Ctrl.$dirty).toBe(false); - }); - - - it('should reset pristine state of anonymous form controls', function() { - - doc = $compile( + input1Ctrl.$setViewValue('xx'); + input2Ctrl.$setViewValue('yy'); + scope.$apply(); + expect(form).toBeDirty(); + expect(input1).toBeDirty(); + expect(input2).toBeDirty(); + + formCtrl.$setPristine(); + expect(form).toBePristine(); + expect(formCtrl.$pristine).toBe(true); + expect(formCtrl.$dirty).toBe(false); + expect(input1).toBePristine(); + expect(input1Ctrl.$pristine).toBe(true); + expect(input1Ctrl.$dirty).toBe(false); + expect(input2).toBePristine(); + expect(input2Ctrl.$pristine).toBe(true); + expect(input2Ctrl.$dirty).toBe(false); + }); + + + it('should reset pristine state of anonymous form controls', function () { + + doc = $compile( '
' + '' + '
')(scope); - scope.$digest(); + scope.$digest(); - var form = doc, + var form = doc, formCtrl = scope.testForm, input = form.find('input').eq(0), inputCtrl = input.controller('ngModel'); - inputCtrl.$setViewValue('xx'); - scope.$apply(); - expect(form).toBeDirty(); - expect(input).toBeDirty(); - - formCtrl.$setPristine(); - expect(form).toBePristine(); - expect(formCtrl.$pristine).toBe(true); - expect(formCtrl.$dirty).toBe(false); - expect(input).toBePristine(); - expect(inputCtrl.$pristine).toBe(true); - expect(inputCtrl.$dirty).toBe(false); - }); + inputCtrl.$setViewValue('xx'); + scope.$apply(); + expect(form).toBeDirty(); + expect(input).toBeDirty(); + formCtrl.$setPristine(); + expect(form).toBePristine(); + expect(formCtrl.$pristine).toBe(true); + expect(formCtrl.$dirty).toBe(false); + expect(input).toBePristine(); + expect(inputCtrl.$pristine).toBe(true); + expect(inputCtrl.$dirty).toBe(false); + }); - it('should reset pristine state of nested forms', function() { - doc = $compile( + it('should reset pristine state of nested forms', function () { + + doc = $compile( '
' + '
' + '' + '
' + '
')(scope); - scope.$digest(); + scope.$digest(); - var form = doc, + var form = doc, formCtrl = scope.testForm, nestedForm = form.find('div'), nestedFormCtrl = nestedForm.controller('form'), nestedInput = form.find('input').eq(0), nestedInputCtrl = nestedInput.controller('ngModel'); - nestedInputCtrl.$setViewValue('xx'); - scope.$apply(); - expect(form).toBeDirty(); - expect(nestedForm).toBeDirty(); - expect(nestedInput).toBeDirty(); - - formCtrl.$setPristine(); - expect(form).toBePristine(); - expect(formCtrl.$pristine).toBe(true); - expect(formCtrl.$dirty).toBe(false); - expect(nestedForm).toBePristine(); - expect(nestedFormCtrl.$pristine).toBe(true); - expect(nestedFormCtrl.$dirty).toBe(false); - expect(nestedInput).toBePristine(); - expect(nestedInputCtrl.$pristine).toBe(true); - expect(nestedInputCtrl.$dirty).toBe(false); + nestedInputCtrl.$setViewValue('xx'); + scope.$apply(); + expect(form).toBeDirty(); + expect(nestedForm).toBeDirty(); + expect(nestedInput).toBeDirty(); + + formCtrl.$setPristine(); + expect(form).toBePristine(); + expect(formCtrl.$pristine).toBe(true); + expect(formCtrl.$dirty).toBe(false); + expect(nestedForm).toBePristine(); + expect(nestedFormCtrl.$pristine).toBe(true); + expect(nestedFormCtrl.$dirty).toBe(false); + expect(nestedInput).toBePristine(); + expect(nestedInputCtrl.$pristine).toBe(true); + expect(nestedInputCtrl.$dirty).toBe(false); + }); + }); + + describe('$setWorking', function () { + + it('should set form $working and $idle and associated css classes', function () { + + doc = $compile( + '
' + + '' + + '' + + '
')(scope); + + scope.$digest(); + + var form = doc, + formCtrl = scope.testForm; + + + formCtrl.$setWorking(); + expect(form).toBeWorking(); + expect(formCtrl.$working).toBe(true); + expect(formCtrl.$idle).toBe(false); + }); + }); + + describe('$setIdle', function () { + + it('should set form $working and $idle and associated css classes', function () { + + doc = $compile( + '
' + + '' + + '' + + '
')(scope); + + scope.$digest(); + + var form = doc, + formCtrl = scope.testForm; + + + formCtrl.$setIdle(); + expect(form).toBeIdle(); + expect(formCtrl.$idle).toBe(true); + expect(formCtrl.$working).toBe(false); + }); + }); + + describe('$setValidating', function () { + + it('should set $validating and ng-validating', function () { + + doc = $compile( + '
' + + '' + + '' + + '
')(scope); + + scope.$digest(); + + var form = doc, + formCtrl = scope.testForm; + + + formCtrl.$setValidating(); + expect(formCtrl.$validating).toBe(true); + expect(doc.hasClass('ng-validating')).toBe(true); + + }); + }); + + describe('$clearValidating', function () { + + it('should clear $validating and ng-validating', function () { + + doc = $compile( + '
' + + '' + + '' + + '
')(scope); + + scope.$digest(); + + var form = doc, + formCtrl = scope.testForm; + + + formCtrl.$clearValidating(); + expect(formCtrl.$validating).toBe(false); + expect(doc.hasClass('ng-validating')).toBe(false); + + }); }); - }); });