Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 3344396

Browse files
eunomiematsko
authored andcommitted
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
1 parent 8794a17 commit 3344396

File tree

5 files changed

+298
-17
lines changed

5 files changed

+298
-17
lines changed

src/ng/directive/form.js

+48-9
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ var nullFormCtrl = {
4646
*
4747
*/
4848
//asks for $scope to fool the BC controller module
49-
FormController.$inject = ['$element', '$attrs', '$scope'];
50-
function FormController(element, attrs) {
49+
FormController.$inject = ['$element', '$attrs', '$scope', '$animate'];
50+
function FormController(element, attrs, $scope, $animate) {
5151
var form = this,
5252
parentForm = element.parent().controller('form') || nullFormCtrl,
5353
invalidCount = 0, // used to easily determine if we are valid
@@ -70,9 +70,8 @@ function FormController(element, attrs) {
7070
// convenience method for easy toggling of classes
7171
function toggleValidCss(isValid, validationErrorKey) {
7272
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
73-
element.
74-
removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
75-
addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
73+
$animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey);
74+
$animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
7675
}
7776

7877
/**
@@ -173,7 +172,8 @@ function FormController(element, attrs) {
173172
* state (ng-dirty class). This method will also propagate to parent forms.
174173
*/
175174
form.$setDirty = function() {
176-
element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
175+
$animate.removeClass(element, PRISTINE_CLASS);
176+
$animate.addClass(element, DIRTY_CLASS);
177177
form.$dirty = true;
178178
form.$pristine = false;
179179
parentForm.$setDirty();
@@ -194,7 +194,8 @@ function FormController(element, attrs) {
194194
* saving or resetting it.
195195
*/
196196
form.$setPristine = function () {
197-
element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS);
197+
$animate.removeClass(element, DIRTY_CLASS);
198+
$animate.addClass(element, PRISTINE_CLASS);
198199
form.$dirty = false;
199200
form.$pristine = true;
200201
forEach(controls, function(control) {
@@ -249,6 +250,8 @@ function FormController(element, attrs) {
249250
* - `ng-pristine` is set if the form is pristine.
250251
* - `ng-dirty` is set if the form is dirty.
251252
*
253+
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
254+
*
252255
*
253256
* # Submitting a form and preventing the default action
254257
*
@@ -282,15 +285,48 @@ function FormController(element, attrs) {
282285
* @param {string=} name Name of the form. If specified, the form controller will be published into
283286
* related scope, under this name.
284287
*
288+
* ## Animation Hooks
289+
*
290+
* Animations in ngForm are triggered when any of the associated CSS classes are added and removed. These
291+
* classes are: `.pristine`, `.dirty`, `.invalid` and `.valid` as well as any other validations that
292+
* are performed within the form. Animations in ngForm are similar to how they work in ngClass and
293+
* animations can be hooked into using CSS transitions, keyframes as well as JS animations.
294+
*
295+
* The following example shows a simple way to utilize CSS transitions to style a form element
296+
* that has been rendered as invalid after it has been validated:
297+
*
298+
* <pre>
299+
* //be sure to include ngAnimate as a module to hook into more
300+
* //advanced animations
301+
* .my-form {
302+
* transition:0.5s linear all;
303+
* background: white;
304+
* }
305+
* .my-form.ng-invalid {
306+
* background: red;
307+
* color:white;
308+
* }
309+
* </pre>
310+
*
285311
* @example
286-
<example>
312+
<example deps="angular-animate.js" animations="true" fixBase="true">
287313
<file name="index.html">
288314
<script>
289315
function Ctrl($scope) {
290316
$scope.userType = 'guest';
291317
}
292318
</script>
293-
<form name="myForm" ng-controller="Ctrl">
319+
<style>
320+
.my-form {
321+
-webkit-transition:all linear 0.5s;
322+
transition:all linear 0.5s;
323+
background: transparent;
324+
}
325+
.my-form.ng-invalid {
326+
background: red;
327+
}
328+
</style>
329+
<form name="myForm" ng-controller="Ctrl" class="my-form">
294330
userType: <input name="input" ng-model="userType" required>
295331
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
296332
<tt>userType = {{userType}}</tt><br>
@@ -322,6 +358,9 @@ function FormController(element, attrs) {
322358
});
323359
</file>
324360
</example>
361+
*
362+
* @param {string=} name Name of the form. If specified, the form controller will be published into
363+
* related scope, under this name.
325364
*/
326365
var formDirectiveFactory = function(isNgForm) {
327366
return ['$timeout', function($timeout) {

src/ng/directive/input.js

+70-8
Original file line numberDiff line numberDiff line change
@@ -1003,8 +1003,8 @@ var VALID_CLASS = 'ng-valid',
10031003
*
10041004
*
10051005
*/
1006-
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse',
1007-
function($scope, $exceptionHandler, $attr, $element, $parse) {
1006+
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate',
1007+
function($scope, $exceptionHandler, $attr, $element, $parse, $animate) {
10081008
this.$viewValue = Number.NaN;
10091009
this.$modelValue = Number.NaN;
10101010
this.$parsers = [];
@@ -1067,9 +1067,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
10671067
// convenience method for easy toggling of classes
10681068
function toggleValidCss(isValid, validationErrorKey) {
10691069
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
1070-
$element.
1071-
removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
1072-
addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
1070+
$animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey);
1071+
$animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
10731072
}
10741073

10751074
/**
@@ -1128,7 +1127,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
11281127
this.$setPristine = function () {
11291128
this.$dirty = false;
11301129
this.$pristine = true;
1131-
$element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS);
1130+
$animate.removeClass($element, DIRTY_CLASS);
1131+
$animate.addClass($element, PRISTINE_CLASS);
11321132
};
11331133

11341134
/**
@@ -1159,7 +1159,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
11591159
if (this.$pristine) {
11601160
this.$dirty = true;
11611161
this.$pristine = false;
1162-
$element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
1162+
$animate.removeClass($element, PRISTINE_CLASS);
1163+
$animate.addClass($element, DIRTY_CLASS);
11631164
parentForm.$setDirty();
11641165
}
11651166

@@ -1225,7 +1226,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
12251226
* require.
12261227
* - Providing validation behavior (i.e. required, number, email, url).
12271228
* - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors).
1228-
* - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`).
1229+
* - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations.
12291230
* - Registering the control with its parent {@link ng.directive:form form}.
12301231
*
12311232
* 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', '$
12481249
* - {@link ng.directive:select select}
12491250
* - {@link ng.directive:textarea textarea}
12501251
*
1252+
* # CSS classes
1253+
* The following CSS classes are added and removed on the associated input/select/textarea element
1254+
* depending on the validity of the model.
1255+
*
1256+
* - `ng-valid` is set if the model is valid.
1257+
* - `ng-invalid` is set if the model is invalid.
1258+
* - `ng-pristine` is set if the model is pristine.
1259+
* - `ng-dirty` is set if the model is dirty.
1260+
*
1261+
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
1262+
*
1263+
* ## Animation Hooks
1264+
*
1265+
* Animations within models are triggered when any of the associated CSS classes are added and removed
1266+
* on the input element which is attached to the model. These classes are: `.pristine`, `.dirty`,
1267+
* `.invalid` and `.valid` as well as any other validations that are performed on the model itself.
1268+
* The animations that are triggered within ngModel are similar to how they work in ngClass and
1269+
* animations can be hooked into using CSS transitions, keyframes as well as JS animations.
1270+
*
1271+
* The following example shows a simple way to utilize CSS transitions to style an input element
1272+
* that has been rendered as invalid after it has been validated:
1273+
*
1274+
* <pre>
1275+
* //be sure to include ngAnimate as a module to hook into more
1276+
* //advanced animations
1277+
* .my-input {
1278+
* transition:0.5s linear all;
1279+
* background: white;
1280+
* }
1281+
* .my-input.ng-invalid {
1282+
* background: red;
1283+
* color:white;
1284+
* }
1285+
* </pre>
1286+
*
1287+
* @example
1288+
* <example deps="angular-animate.js" animations="true" fixBase="true">
1289+
<file name="index.html">
1290+
<script>
1291+
function Ctrl($scope) {
1292+
$scope.val = '1';
1293+
}
1294+
</script>
1295+
<style>
1296+
.my-input {
1297+
-webkit-transition:all linear 0.5s;
1298+
transition:all linear 0.5s;
1299+
background: transparent;
1300+
}
1301+
.my-input.ng-invalid {
1302+
color:white;
1303+
background: red;
1304+
}
1305+
</style>
1306+
Update input to see transitions when valid/invalid.
1307+
Integer is a valid value.
1308+
<form name="testForm" ng-controller="Ctrl">
1309+
<input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" />
1310+
</form>
1311+
</file>
1312+
* </example>
12511313
*/
12521314
var ngModelDirective = function() {
12531315
return {

src/ngAnimate/animate.js

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
* | {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave |
3232
* | {@link ng.directive:ngClass#usage_animations ngClass} | add and remove |
3333
* | {@link ng.directive:ngShow#usage_animations ngShow & ngHide} | add and remove (the ng-hide class value) |
34+
* | {@link ng.directive:form#usage_animations form} | add and remove (dirty, pristine, valid, invalid & all other validations) |
35+
* | {@link ng.directive:ngModel#usage_animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
3436
*
3537
* You can find out more information about animations upon visiting each directive page.
3638
*

test/ng/directive/formSpec.js

+80
Original file line numberDiff line numberDiff line change
@@ -594,3 +594,83 @@ describe('form', function() {
594594
});
595595
});
596596
});
597+
598+
describe('form animations', function() {
599+
beforeEach(module('ngAnimateMock'));
600+
601+
function assertValidAnimation(animation, event, className) {
602+
expect(animation.event).toBe(event);
603+
expect(animation.args[1]).toBe(className);
604+
}
605+
606+
var doc, scope, form;
607+
beforeEach(inject(function($rootScope, $compile, $rootElement, $animate) {
608+
scope = $rootScope.$new();
609+
doc = jqLite('<form name="myForm"></form>');
610+
$rootElement.append(doc);
611+
$compile(doc)(scope);
612+
$animate.queue = [];
613+
form = scope.myForm;
614+
}));
615+
616+
afterEach(function() {
617+
dealoc(doc);
618+
});
619+
620+
it('should trigger an animation when invalid', inject(function($animate) {
621+
form.$setValidity('required', false);
622+
623+
assertValidAnimation($animate.queue[0], 'removeClass', 'ng-valid');
624+
assertValidAnimation($animate.queue[1], 'addClass', 'ng-invalid');
625+
assertValidAnimation($animate.queue[2], 'removeClass', 'ng-valid-required');
626+
assertValidAnimation($animate.queue[3], 'addClass', 'ng-invalid-required');
627+
}));
628+
629+
it('should trigger an animation when valid', inject(function($animate) {
630+
form.$setValidity('required', false);
631+
632+
$animate.queue = [];
633+
634+
form.$setValidity('required', true);
635+
636+
assertValidAnimation($animate.queue[0], 'removeClass', 'ng-invalid');
637+
assertValidAnimation($animate.queue[1], 'addClass', 'ng-valid');
638+
assertValidAnimation($animate.queue[2], 'removeClass', 'ng-invalid-required');
639+
assertValidAnimation($animate.queue[3], 'addClass', 'ng-valid-required');
640+
}));
641+
642+
it('should trigger an animation when dirty', inject(function($animate) {
643+
form.$setDirty();
644+
645+
assertValidAnimation($animate.queue[0], 'removeClass', 'ng-pristine');
646+
assertValidAnimation($animate.queue[1], 'addClass', 'ng-dirty');
647+
}));
648+
649+
it('should trigger an animation when pristine', inject(function($animate) {
650+
form.$setDirty();
651+
652+
$animate.queue = [];
653+
654+
form.$setPristine();
655+
656+
assertValidAnimation($animate.queue[0], 'removeClass', 'ng-dirty');
657+
assertValidAnimation($animate.queue[1], 'addClass', 'ng-pristine');
658+
}));
659+
660+
it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) {
661+
form.$setValidity('custom-error', false);
662+
663+
assertValidAnimation($animate.queue[0], 'removeClass', 'ng-valid');
664+
assertValidAnimation($animate.queue[1], 'addClass', 'ng-invalid');
665+
assertValidAnimation($animate.queue[2], 'removeClass', 'ng-valid-custom-error');
666+
assertValidAnimation($animate.queue[3], 'addClass', 'ng-invalid-custom-error');
667+
668+
$animate.queue = [];
669+
form.$setValidity('custom-error', true);
670+
671+
assertValidAnimation($animate.queue[0], 'removeClass', 'ng-invalid');
672+
assertValidAnimation($animate.queue[1], 'addClass', 'ng-valid');
673+
assertValidAnimation($animate.queue[2], 'removeClass', 'ng-invalid-custom-error');
674+
assertValidAnimation($animate.queue[3], 'addClass', 'ng-valid-custom-error');
675+
}));
676+
});

0 commit comments

Comments
 (0)