diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 06ffad868d0c..1a13adea79a2 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -46,8 +46,8 @@ var nullFormCtrl = { * */ //asks for $scope to fool the BC controller module -FormController.$inject = ['$element', '$attrs', '$scope']; -function FormController(element, attrs) { +FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; +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 @@ -70,9 +70,8 @@ function FormController(element, attrs) { // 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); + $animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); + $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); } /** @@ -177,7 +176,8 @@ function FormController(element, attrs) { * state (ng-dirty class). This method will also propagate to parent forms. */ form.$setDirty = function() { - element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); + $animate.removeClass(element, PRISTINE_CLASS); + $animate.addClass(element, DIRTY_CLASS); form.$dirty = true; form.$pristine = false; parentForm.$setDirty(); @@ -199,7 +199,8 @@ function FormController(element, attrs) { * saving or resetting it. */ form.$setPristine = function () { - element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); + $animate.removeClass(element, DIRTY_CLASS); + $animate.addClass(element, PRISTINE_CLASS); form.$dirty = false; form.$pristine = true; forEach(controls, function(control) { @@ -284,8 +285,28 @@ function FormController(element, attrs) { * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * ## A note about animations with `ngForm` + * + * Animations in ngForm work with the pristine, dirty, invalid and valid events that are triggered when + * the values of form change. This system works like the animation system present with ngClass. + * + *
+ * //
+ * //a working example can be found at the bottom of this page
+ * //
+ * .my-element.ng-dirty-add {
+ *   transition:0.5s linear all;
+ *   background: red;
+ * }
+ * .my-element.ng-dirty {
+ *   background: white;
+ * }
+ *
+ * .my-element.ng-dirty-add { ... }
+ * .my-element.ng-dirty-add.ng-dirty-add-active { ... }
+ * .my-element.ng-dirty-remove { ... }
+ * .my-element.ng-dirty-remove.ng-dirty-remove-active { ... }
+ * 
* * @example @@ -295,6 +316,16 @@ function FormController(element, attrs) { $scope.userType = 'guest'; } +
userType: Required!
@@ -318,6 +349,15 @@ function FormController(element, attrs) { }); + * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. + * + * @animations + * removeClass .ng-dirty and addClass .ng-pristine: happens just after form became pristine + * removeClass .ng-pristine and addClass .ng-dirty: happens just after form became dirty + * removeClass .ng-invalid and addClass .ng-valid: happens just after form became valid + * removeClass .ng-valid and addClass .ng-invalid: happens just after form became invalid */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 4e77397878dd..724c974fe74f 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -915,8 +915,8 @@ var VALID_CLASS = 'ng-valid', * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', - function($scope, $exceptionHandler, $attr, $element, $parse) { +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', + function($scope, $exceptionHandler, $attr, $element, $parse, $animate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$parsers = []; @@ -978,9 +978,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$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); + $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); + $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); } /** @@ -1041,7 +1040,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$setPristine = function () { this.$dirty = false; this.$pristine = true; - $element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); + $animate.removeClass($element, DIRTY_CLASS); + $animate.addClass($element, PRISTINE_CLASS); }; /** @@ -1073,7 +1073,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (this.$pristine) { this.$dirty = true; this.$pristine = false; - $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); + $animate.removeClass($element, PRISTINE_CLASS); + $animate.addClass($element, DIRTY_CLASS); parentForm.$setDirty(); } @@ -1139,7 +1140,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * require. * - Providing validation behavior (i.e. required, number, email, url). * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations. * - Registering the control with its parent {@link ng.directive:form form}. * * Note: `ngModel` will try to bind to the property given by evaluating the expression on the @@ -1162,6 +1163,60 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * + * ## A note about animations with `NgModel` + * + * Animations work with the pristine, dirty, invalid and valid events that are triggered when + * the values of input change. This system works like the animation system present with ngClass. + * + *
+ * //
+ * //a working example can be found at the bottom of this page
+ * //
+ * .my-element.ng-dirty-add {
+ *   transition:0.5s linear all;
+ *   background: red;
+ * }
+ * .my-element.ng-dirty {
+ *   background: white;
+ * }
+ *
+ * .my-element.ng-dirty-add { ... }
+ * .my-element.ng-dirty-add.ng-dirty-add-active { ... }
+ * .my-element.ng-dirty-remove { ... }
+ * .my-element.ng-dirty-remove.ng-dirty-remove-active { ... }
+ * 
+ * + * @animations + * removeClass .ng-dirty and addClass .ng-pristine: happens just after input became pristine + * removeClass .ng-pristine and addClass .ng-dirty: happens just after input became dirty + * removeClass .ng-invalid and addClass .ng-valid: happens just after input became valid + * removeClass .ng-valid and addClass .ng-invalid: happens just after input became invalid + * + * @example + * + + + + Update input to see transitions when valid/invalid. + Integer is a valid value. + + + + + *
*/ var ngModelDirective = function() { return { diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 64ed302981a9..8a65f74079a7 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -32,6 +32,8 @@ * | {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave | * | {@link ng.directive:ngClass#usage_animations ngClass} | add and remove | * | {@link ng.directive:ngShow#usage_animations ngShow & ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#usage_animations form} | dirty, pristine, valid and invalid | + * | {@link ng.directive:ngModel#usage_animations ngModel} | dirty, pristine, valid and invalid | * * You can find out more information about animations upon visiting each directive page. * diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index dde6f0a026c8..97c148f380cb 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -594,3 +594,109 @@ describe('form', function() { }); }); }); + +describe('form animations', function() { + var body, element, $rootElement, changeInputValue; + + function html(html) { + body.append($rootElement); + $rootElement.html(html); + element = $rootElement.children().eq(0); + return element; + } + + beforeEach(function() { + // we need to run animation on attached elements; + body = jqLite(document.body); + }); + + afterEach(function(){ + dealoc(body); + dealoc(element); + body.removeAttr('ng-animation-running'); + }); + + beforeEach(module('mock.animate')); + + beforeEach(module(function($animateProvider, $provide) { + return function(_$rootElement_) { + $rootElement = _$rootElement_; + }; + })); + + describe('dirty/pristine/valid/invalid states', function() { + it('should fire animations', inject(function($compile, $rootScope, $animate) { + var flushAnimates = function() { + $animate.flushNext('removeClass'); + $animate.flushNext('addClass'); + }; + + var $scope = $rootScope.$new(); + $scope.name = "bad value"; + element = $compile(html( + '
' + + '' + + '
' + ))($scope); + + $scope.$digest(); + + var form = element, + formCtrl = $scope.testForm, + input = form.find('input').eq(0), + inputCtrl = input.controller('ngModel'); + + expect($scope.name).toBe('bad value'); + expect(input).toBePristine(); + expect(form).toBePristine(); + + flushAnimates(); + expect(form).toBeValid(); + + flushAnimates(); + expect(input).toBeValid(); + + flushAnimates(); + expect(input).toBeInvalid(); + + flushAnimates(); + expect(input).toHaveClass('ng-invalid-pattern'); + + flushAnimates(); + expect(form).toBeInvalid(); + + flushAnimates(); + expect(form).toHaveClass('ng-invalid-pattern'); + + inputCtrl.$setViewValue('x'); + $scope.$digest(); + expect($scope.name).toBe('x'); + + flushAnimates(); + expect(input).toBeDirty(); + + flushAnimates(); + expect(form).toBeDirty(); + + flushAnimates(); + expect(input).toBeValid(); + + flushAnimates(); + expect(input).not.toHaveClass('ng-invalid-pattern'); + + flushAnimates(); + expect(form).toBeValid(); + + flushAnimates(); + expect(form).not.toHaveClass('ng-invalid-pattern'); + + formCtrl.$setPristine(); + + flushAnimates(); + expect(form).toBePristine(); + + flushAnimates(); + expect(input).toBePristine(); + })); + }); +});