Permalink
Browse files

feat($animate): animate dirty, pristine, valid, invalid for form/fields

Add css animations when form or field status change to/from dirty,
pristine, valid or invalid. This works like animation system present
with ngClass, ngShow, etc.

Closes #5378
  • Loading branch information...
eunomie authored and matsko committed Dec 10, 2013
1 parent 8794a17 commit 33443966c8e8cac85a863bb181d4a4aff00baab4
Showing with 298 additions and 17 deletions.
  1. +48 −9 src/ng/directive/form.js
  2. +70 −8 src/ng/directive/input.js
  3. +2 −0 src/ngAnimate/animate.js
  4. +80 −0 test/ng/directive/formSpec.js
  5. +98 −0 test/ng/directive/inputSpec.js
View
@@ -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
*
@@ -282,15 +285,48 @@ 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.
*
+ * ## 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

This comment has been minimized.

Show comment
Hide comment
@cburgdorf

cburgdorf Feb 28, 2014

Contributor

shouldn't this be .ng-pristine, .ng-dirty, .ng-invalid and .ng-valid?

@cburgdorf

cburgdorf Feb 28, 2014

Contributor

shouldn't this be .ng-pristine, .ng-dirty, .ng-invalid and .ng-valid?

This comment has been minimized.

Show comment
Hide comment
@matsko

matsko Feb 28, 2014

Member

Ahh yes. Good point.

@matsko

matsko Feb 28, 2014

Member

Ahh yes. Good point.

This comment has been minimized.

Show comment
Hide comment
@matsko

matsko Feb 28, 2014

Member

Fixed. 0d034a9

Thanks!

@matsko

matsko Feb 28, 2014

Member

Fixed. 0d034a9

Thanks!

+ * 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:
+ *
+ * <pre>
+ * //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;
+ * }
+ * </pre>
+ *
* @example
- <example>
+ <example deps="angular-animate.js" animations="true" fixBase="true">
<file name="index.html">
<script>
function Ctrl($scope) {
$scope.userType = 'guest';
}
</script>
- <form name="myForm" ng-controller="Ctrl">
+ <style>
+ .my-form {
+ -webkit-transition:all linear 0.5s;
+ transition:all linear 0.5s;
+ background: transparent;
+ }
+ .my-form.ng-invalid {
+ background: red;
+ }
+ </style>
+ <form name="myForm" ng-controller="Ctrl" class="my-form">
userType: <input name="input" ng-model="userType" required>
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
<tt>userType = {{userType}}</tt><br>
@@ -322,6 +358,9 @@ function FormController(element, attrs) {
});
</file>
</example>
+ *
+ * @param {string=} name Name of the form. If specified, the form controller will be published into

This comment has been minimized.

Show comment
Hide comment
@al-the-x

al-the-x Mar 17, 2014

Contributor

Unintentional duplicate from 285? See #6720 for fix...

@al-the-x

al-the-x Mar 17, 2014

Contributor

Unintentional duplicate from 285? See #6720 for fix...

+ * related scope, under this name.
*/
var formDirectiveFactory = function(isNgForm) {
return ['$timeout', function($timeout) {
View
@@ -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:
+ *
+ * <pre>
+ * //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;
+ * }
+ * </pre>
+ *
+ * @example
+ * <example deps="angular-animate.js" animations="true" fixBase="true">
+ <file name="index.html">
+ <script>
+ function Ctrl($scope) {
+ $scope.val = '1';
+ }
+ </script>
+ <style>
+ .my-input {
+ -webkit-transition:all linear 0.5s;
+ transition:all linear 0.5s;
+ background: transparent;
+ }
+ .my-input.ng-invalid {
+ color:white;
+ background: red;
+ }
+ </style>
+ Update input to see transitions when valid/invalid.
+ Integer is a valid value.
+ <form name="testForm" ng-controller="Ctrl">
+ <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" />
+ </form>
+ </file>
+ * </example>
*/
var ngModelDirective = function() {
return {
View
@@ -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} | add and remove (dirty, pristine, valid, invalid & all other validations) |
+ * | {@link ng.directive:ngModel#usage_animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
*
* You can find out more information about animations upon visiting each directive page.
*
@@ -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('<form name="myForm"></form>');
+ $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');
+ }));
+});
Oops, something went wrong.

0 comments on commit 3344396

Please sign in to comment.