Skip to content

Commit 5f4d8ea

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 angular#5378
1 parent 8794a17 commit 5f4d8ea

File tree

5 files changed

+292
-17
lines changed

5 files changed

+292
-17
lines changed

src/ng/directive/form.js

+49-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) {
@@ -279,8 +280,28 @@ function FormController(element, attrs) {
279280
* hitting enter in any of the input fields will trigger the click handler on the *first* button or
280281
* input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`)
281282
*
282-
* @param {string=} name Name of the form. If specified, the form controller will be published into
283-
* related scope, under this name.
283+
* ## A note about animations with `ngForm`
284+
*
285+
* Animations in ngForm work with the pristine, dirty, invalid and valid events that are triggered when
286+
* the values of form change. This system works like the animation system present with ngClass.
287+
*
288+
* <pre>
289+
* //
290+
* //a working example can be found at the bottom of this page
291+
* //
292+
* .my-element.ng-dirty-add {
293+
* transition:0.5s linear all;
294+
* background: red;
295+
* }
296+
* .my-element.ng-dirty {
297+
* background: white;
298+
* }
299+
*
300+
* .my-element.ng-dirty-add { ... }
301+
* .my-element.ng-dirty-add.ng-dirty-add-active { ... }
302+
* .my-element.ng-dirty-remove { ... }
303+
* .my-element.ng-dirty-remove.ng-dirty-remove-active { ... }
304+
* </pre>
284305
*
285306
* @example
286307
<example>
@@ -290,6 +311,16 @@ function FormController(element, attrs) {
290311
$scope.userType = 'guest';
291312
}
292313
</script>
314+
<style>
315+
form.ng-dirty-add {
316+
-webkit-transition:all linear 0.5s;
317+
transition:all linear 0.5s;
318+
background: orange;
319+
}
320+
form.ng-dirty {
321+
background: transparent;
322+
}
323+
</style>
293324
<form name="myForm" ng-controller="Ctrl">
294325
userType: <input name="input" ng-model="userType" required>
295326
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
@@ -322,6 +353,15 @@ function FormController(element, attrs) {
322353
});
323354
</file>
324355
</example>
356+
*
357+
* @param {string=} name Name of the form. If specified, the form controller will be published into
358+
* related scope, under this name.
359+
*
360+
* @animations
361+
* removeClass .ng-dirty and addClass .ng-pristine: happens just after form became pristine
362+
* removeClass .ng-pristine and addClass .ng-dirty: happens just after form became dirty
363+
* removeClass .ng-invalid and addClass .ng-valid: happens just after form became valid
364+
* removeClass .ng-valid and addClass .ng-invalid: happens just after form became invalid
325365
*/
326366
var formDirectiveFactory = function(isNgForm) {
327367
return ['$timeout', function($timeout) {

src/ng/directive/input.js

+63-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,60 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
12481249
* - {@link ng.directive:select select}
12491250
* - {@link ng.directive:textarea textarea}
12501251
*
1252+
* ## A note about animations with `NgModel`
1253+
*
1254+
* Animations work with the pristine, dirty, invalid and valid events that are triggered when
1255+
* the values of input change. This system works like the animation system present with ngClass.
1256+
*
1257+
* <pre>
1258+
* //
1259+
* //a working example can be found at the bottom of this page
1260+
* //
1261+
* .my-element.ng-dirty-add {
1262+
* transition:0.5s linear all;
1263+
* background: red;
1264+
* }
1265+
* .my-element.ng-dirty {
1266+
* background: white;
1267+
* }
1268+
*
1269+
* .my-element.ng-dirty-add { ... }
1270+
* .my-element.ng-dirty-add.ng-dirty-add-active { ... }
1271+
* .my-element.ng-dirty-remove { ... }
1272+
* .my-element.ng-dirty-remove.ng-dirty-remove-active { ... }
1273+
* </pre>
1274+
*
1275+
* @animations
1276+
* removeClass .ng-dirty and addClass .ng-pristine: happens just after input became pristine
1277+
* removeClass .ng-pristine and addClass .ng-dirty: happens just after input became dirty
1278+
* removeClass .ng-invalid and addClass .ng-valid: happens just after input became valid
1279+
* removeClass .ng-valid and addClass .ng-invalid: happens just after input became invalid
1280+
*
1281+
* @example
1282+
* <doc:example>
1283+
<doc:source>
1284+
<script>
1285+
function Ctrl($scope) {
1286+
$scope.val = '1';
1287+
}
1288+
</script>
1289+
<style>
1290+
input.ng-invalid-pattern-add {
1291+
-webkit-transition:all linear 0.5s;
1292+
transition:all linear 0.5s;
1293+
background: red;
1294+
}
1295+
input.ng-invalid {
1296+
background: white;
1297+
}
1298+
</style>
1299+
Update input to see transitions when valid/invalid.
1300+
Integer is a valid value.
1301+
<form name="testForm" ng-controller="Ctrl">
1302+
<input ng-model="val" ng-pattern="/^\d+$/" name="anim"/>
1303+
</form>
1304+
</doc:source>
1305+
* </doc:example>
12511306
*/
12521307
var ngModelDirective = function() {
12531308
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} | dirty, pristine, valid and invalid |
35+
* | {@link ng.directive:ngModel#usage_animations ngModel} | dirty, pristine, valid and invalid |
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)