diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index c5e39cde310a..4af109cbe8d7 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); } /** @@ -173,7 +172,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(); @@ -194,7 +194,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) { @@ -249,6 +250,8 @@ function FormController(element, attrs) { * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * * * # Submitting a form and preventing the default action * @@ -279,18 +282,48 @@ 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. + * ## Animation Hooks + * + * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. These + * classes are: `.pristine`, `.dirty`, `.invalid` and `.valid` as well as any other validations that + * are performed within the form. Animations in ngForm are similar to how they work in ngClass and + * animations can be hooked into using CSS transitions, keyframes as well as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style a form element + * that has been rendered as invalid after it has been validated: + * + *
+ * //be sure to include ngAnimate as a module to hook into more
+ * //advanced animations
+ * .my-form {
+ *   transition:0.5s linear all;
+ *   background: white;
+ * }
+ * .my-form.ng-invalid {
+ *   background: red;
+ *   color:white;
+ * }
+ * 
* * @example - + -
+ + userType: Required!
userType = {{userType}}
@@ -322,6 +355,9 @@ 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. */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index d6ee26beb274..f584eda4c6db 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1003,8 +1003,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 = []; @@ -1067,9 +1067,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); } /** @@ -1128,7 +1127,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); }; /** @@ -1159,7 +1159,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(); } @@ -1225,7 +1226,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 @@ -1248,6 +1249,67 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * + * # CSS classes + * The following CSS classes are added and removed on the associated input/select/textarea element + * depending on the validity of the model. + * + * - `ng-valid` is set if the model is valid. + * - `ng-invalid` is set if the model is invalid. + * - `ng-pristine` is set if the model is pristine. + * - `ng-dirty` is set if the model is dirty. + * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * + * ## Animation Hooks + * + * Animations within models are triggered when any of the associated CSS classes are added and removed + * on the input element which is attached to the model. These classes are: `.pristine`, `.dirty`, + * `.invalid` and `.valid` as well as any other validations that are performed on the model itself. + * The animations that are triggered within ngModel are similar to how they work in ngClass and + * animations can be hooked into using CSS transitions, keyframes as well as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style an input element + * that has been rendered as invalid after it has been validated: + * + *
+ * //be sure to include ngAnimate as a module to hook into more
+ * //advanced animations
+ * .my-input {
+ *   transition:0.5s linear all;
+ *   background: white;
+ * }
+ * .my-input.ng-invalid {
+ *   background: red;
+ *   color:white;
+ * }
+ * 
+ * + * @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 b4baf3e296e9..df35c1f6643e 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -31,6 +31,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..b55d1f8d1a78 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -594,3 +594,83 @@ describe('form', function() { }); }); }); + +describe('form animations', function() { + beforeEach(module('ngAnimateMock')); + + function assertValidAnimation(animation, event, className) { + expect(animation.event).toBe(event); + expect(animation.args[1]).toBe(className); + } + + var doc, scope, form; + beforeEach(inject(function($rootScope, $compile, $rootElement, $animate) { + scope = $rootScope.$new(); + doc = jqLite('
'); + $rootElement.append(doc); + $compile(doc)(scope); + $animate.queue = []; + form = scope.myForm; + })); + + afterEach(function() { + dealoc(doc); + }); + + it('should trigger an animation when invalid', inject(function($animate) { + form.$setValidity('required', false); + + assertValidAnimation($animate.queue[0], 'removeClass', 'ng-valid'); + assertValidAnimation($animate.queue[1], 'addClass', 'ng-invalid'); + assertValidAnimation($animate.queue[2], 'removeClass', 'ng-valid-required'); + assertValidAnimation($animate.queue[3], 'addClass', 'ng-invalid-required'); + })); + + it('should trigger an animation when valid', inject(function($animate) { + form.$setValidity('required', false); + + $animate.queue = []; + + form.$setValidity('required', true); + + assertValidAnimation($animate.queue[0], 'removeClass', 'ng-invalid'); + assertValidAnimation($animate.queue[1], 'addClass', 'ng-valid'); + assertValidAnimation($animate.queue[2], 'removeClass', 'ng-invalid-required'); + assertValidAnimation($animate.queue[3], 'addClass', 'ng-valid-required'); + })); + + it('should trigger an animation when dirty', inject(function($animate) { + form.$setDirty(); + + assertValidAnimation($animate.queue[0], 'removeClass', 'ng-pristine'); + assertValidAnimation($animate.queue[1], 'addClass', 'ng-dirty'); + })); + + it('should trigger an animation when pristine', inject(function($animate) { + form.$setDirty(); + + $animate.queue = []; + + form.$setPristine(); + + assertValidAnimation($animate.queue[0], 'removeClass', 'ng-dirty'); + assertValidAnimation($animate.queue[1], 'addClass', 'ng-pristine'); + })); + + it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) { + form.$setValidity('custom-error', false); + + assertValidAnimation($animate.queue[0], 'removeClass', 'ng-valid'); + assertValidAnimation($animate.queue[1], 'addClass', 'ng-invalid'); + assertValidAnimation($animate.queue[2], 'removeClass', 'ng-valid-custom-error'); + assertValidAnimation($animate.queue[3], 'addClass', 'ng-invalid-custom-error'); + + $animate.queue = []; + form.$setValidity('custom-error', true); + + assertValidAnimation($animate.queue[0], 'removeClass', 'ng-invalid'); + assertValidAnimation($animate.queue[1], 'addClass', 'ng-valid'); + assertValidAnimation($animate.queue[2], 'removeClass', 'ng-invalid-custom-error'); + assertValidAnimation($animate.queue[3], 'addClass', 'ng-valid-custom-error'); + })); +}); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index b9f737ac0593..e3e50e02a69e 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1482,3 +1482,101 @@ describe('input', function() { }); }); }); + +describe('NgModel animations', function() { + beforeEach(module('ngAnimateMock')); + + function findElementAnimations(element, queue) { + var node = element[0]; + var animations = []; + for(var i = 0; i < queue.length; i++) { + var animation = queue[i]; + if(animation.element[0] == node) { + animations.push(animation); + } + } + return animations; + }; + + function assertValidAnimation(animation, event, className) { + expect(animation.event).toBe(event); + expect(animation.args[1]).toBe(className); + } + + var doc, input, scope, model; + beforeEach(inject(function($rootScope, $compile, $rootElement, $animate) { + scope = $rootScope.$new(); + doc = jqLite('
' + + ' ' + + '
'); + $rootElement.append(doc); + $compile(doc)(scope); + $animate.queue = []; + + input = doc.find('input'); + model = scope.myForm.myInput; + })); + + afterEach(function() { + dealoc(input); + }); + + it('should trigger an animation when invalid', inject(function($animate) { + model.$setValidity('required', false); + + var animations = findElementAnimations(input, $animate.queue); + assertValidAnimation(animations[0], 'removeClass', 'ng-valid'); + assertValidAnimation(animations[1], 'addClass', 'ng-invalid'); + assertValidAnimation(animations[2], 'removeClass', 'ng-valid-required'); + assertValidAnimation(animations[3], 'addClass', 'ng-invalid-required'); + })); + + it('should trigger an animation when valid', inject(function($animate) { + model.$setValidity('required', false); + + $animate.queue = []; + + model.$setValidity('required', true); + + var animations = findElementAnimations(input, $animate.queue); + assertValidAnimation(animations[0], 'removeClass', 'ng-invalid'); + assertValidAnimation(animations[1], 'addClass', 'ng-valid'); + assertValidAnimation(animations[2], 'removeClass', 'ng-invalid-required'); + assertValidAnimation(animations[3], 'addClass', 'ng-valid-required'); + })); + + it('should trigger an animation when dirty', inject(function($animate) { + model.$setViewValue('some dirty value'); + + var animations = findElementAnimations(input, $animate.queue); + assertValidAnimation(animations[0], 'removeClass', 'ng-pristine'); + assertValidAnimation(animations[1], 'addClass', 'ng-dirty'); + })); + + it('should trigger an animation when pristine', inject(function($animate) { + model.$setPristine(); + + var animations = findElementAnimations(input, $animate.queue); + assertValidAnimation(animations[0], 'removeClass', 'ng-dirty'); + assertValidAnimation(animations[1], 'addClass', 'ng-pristine'); + })); + + it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) { + model.$setValidity('custom-error', false); + + var animations = findElementAnimations(input, $animate.queue); + assertValidAnimation(animations[0], 'removeClass', 'ng-valid'); + assertValidAnimation(animations[1], 'addClass', 'ng-invalid'); + assertValidAnimation(animations[2], 'removeClass', 'ng-valid-custom-error'); + assertValidAnimation(animations[3], 'addClass', 'ng-invalid-custom-error'); + + $animate.queue = []; + model.$setValidity('custom-error', true); + + animations = findElementAnimations(input, $animate.queue); + assertValidAnimation(animations[0], 'removeClass', 'ng-invalid'); + assertValidAnimation(animations[1], 'addClass', 'ng-valid'); + assertValidAnimation(animations[2], 'removeClass', 'ng-invalid-custom-error'); + assertValidAnimation(animations[3], 'addClass', 'ng-valid-custom-error'); + })); +});