Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 558f78f

Browse files
eunomiematsko
authored andcommittedFeb 28, 2014
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 558f78f

File tree

5 files changed

+297
-19
lines changed

5 files changed

+297
-19
lines changed
 

‎src/ng/directive/form.js

+47-11
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
*
@@ -279,18 +282,48 @@ function FormController(element, attrs) {
279282
* hitting enter in any of the input fields will trigger the click handler on the *first* button or
280283
* input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`)
281284
*
282-
* @param {string=} name Name of the form. If specified, the form controller will be published into
283-
* related scope, under this name.
285+
* ## Animation Hooks
286+
*
287+
* Animations in ngForm are triggered when any of the associated CSS classes are added and removed. These
288+
* classes are: `.pristine`, `.dirty`, `.invalid` and `.valid` as well as any other validations that
289+
* are performed within the form. Animations in ngForm are similar to how they work in ngClass and
290+
* animations can be hooked into using CSS transitions, keyframes as well as JS animations.
291+
*
292+
* The following example shows a simple way to utilize CSS transitions to style a form element
293+
* that has been rendered as invalid after it has been validated:
294+
*
295+
* <pre>
296+
* //be sure to include ngAnimate as a module to hook into more
297+
* //advanced animations
298+
* .my-form {
299+
* transition:0.5s linear all;
300+
* background: white;
301+
* }
302+
* .my-form.ng-invalid {
303+
* background: red;
304+
* color:white;
305+
* }
306+
* </pre>
284307
*
285308
* @example
286-
<example>
309+
<example deps="angular-animate.js" animations="true" fixBase="true">
287310
<file name="index.html">
288311
<script>
289312
function Ctrl($scope) {
290313
$scope.userType = 'guest';
291314
}
292315
</script>
293-
<form name="myForm" ng-controller="Ctrl">
316+
<style>
317+
.my-form {
318+
-webkit-transition:all linear 0.5s;
319+
transition:all linear 0.5s;
320+
background: transparent;
321+
}
322+
.my-form.ng-invalid {
323+
background: red;
324+
}
325+
</style>
326+
<form name="myForm" ng-controller="Ctrl" class="my-form">
294327
userType: <input name="input" ng-model="userType" required>
295328
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
296329
<tt>userType = {{userType}}</tt><br>
@@ -322,6 +355,9 @@ function FormController(element, attrs) {
322355
});
323356
</file>
324357
</example>
358+
*
359+
* @param {string=} name Name of the form. If specified, the form controller will be published into
360+
* related scope, under this name.
325361
*/
326362
var formDirectiveFactory = function(isNgForm) {
327363
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+
});

‎test/ng/directive/inputSpec.js

+98
Original file line numberDiff line numberDiff line change
@@ -1482,3 +1482,101 @@ describe('input', function() {
14821482
});
14831483
});
14841484
});
1485+
1486+
describe('NgModel animations', function() {
1487+
beforeEach(module('ngAnimateMock'));
1488+
1489+
function findElementAnimations(element, queue) {
1490+
var node = element[0];
1491+
var animations = [];
1492+
for(var i = 0; i < queue.length; i++) {
1493+
var animation = queue[i];
1494+
if(animation.element[0] == node) {
1495+
animations.push(animation);
1496+
}
1497+
}
1498+
return animations;
1499+
};
1500+
1501+
function assertValidAnimation(animation, event, className) {
1502+
expect(animation.event).toBe(event);
1503+
expect(animation.args[1]).toBe(className);
1504+
}
1505+
1506+
var doc, input, scope, model;
1507+
beforeEach(inject(function($rootScope, $compile, $rootElement, $animate) {
1508+
scope = $rootScope.$new();
1509+
doc = jqLite('<form name="myForm">' +
1510+
' <input type="text" ng-model="input" name="myInput" />' +
1511+
'</form>');
1512+
$rootElement.append(doc);
1513+
$compile(doc)(scope);
1514+
$animate.queue = [];
1515+
1516+
input = doc.find('input');
1517+
model = scope.myForm.myInput;
1518+
}));
1519+
1520+
afterEach(function() {
1521+
dealoc(input);
1522+
});
1523+
1524+
it('should trigger an animation when invalid', inject(function($animate) {
1525+
model.$setValidity('required', false);
1526+
1527+
var animations = findElementAnimations(input, $animate.queue);
1528+
assertValidAnimation(animations[0], 'removeClass', 'ng-valid');
1529+
assertValidAnimation(animations[1], 'addClass', 'ng-invalid');
1530+
assertValidAnimation(animations[2], 'removeClass', 'ng-valid-required');
1531+
assertValidAnimation(animations[3], 'addClass', 'ng-invalid-required');
1532+
}));
1533+
1534+
it('should trigger an animation when valid', inject(function($animate) {
1535+
model.$setValidity('required', false);
1536+
1537+
$animate.queue = [];
1538+
1539+
model.$setValidity('required', true);
1540+
1541+
var animations = findElementAnimations(input, $animate.queue);
1542+
assertValidAnimation(animations[0], 'removeClass', 'ng-invalid');
1543+
assertValidAnimation(animations[1], 'addClass', 'ng-valid');
1544+
assertValidAnimation(animations[2], 'removeClass', 'ng-invalid-required');
1545+
assertValidAnimation(animations[3], 'addClass', 'ng-valid-required');
1546+
}));
1547+
1548+
it('should trigger an animation when dirty', inject(function($animate) {
1549+
model.$setViewValue('some dirty value');
1550+
1551+
var animations = findElementAnimations(input, $animate.queue);
1552+
assertValidAnimation(animations[0], 'removeClass', 'ng-pristine');
1553+
assertValidAnimation(animations[1], 'addClass', 'ng-dirty');
1554+
}));
1555+
1556+
it('should trigger an animation when pristine', inject(function($animate) {
1557+
model.$setPristine();
1558+
1559+
var animations = findElementAnimations(input, $animate.queue);
1560+
assertValidAnimation(animations[0], 'removeClass', 'ng-dirty');
1561+
assertValidAnimation(animations[1], 'addClass', 'ng-pristine');
1562+
}));
1563+
1564+
it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) {
1565+
model.$setValidity('custom-error', false);
1566+
1567+
var animations = findElementAnimations(input, $animate.queue);
1568+
assertValidAnimation(animations[0], 'removeClass', 'ng-valid');
1569+
assertValidAnimation(animations[1], 'addClass', 'ng-invalid');
1570+
assertValidAnimation(animations[2], 'removeClass', 'ng-valid-custom-error');
1571+
assertValidAnimation(animations[3], 'addClass', 'ng-invalid-custom-error');
1572+
1573+
$animate.queue = [];
1574+
model.$setValidity('custom-error', true);
1575+
1576+
animations = findElementAnimations(input, $animate.queue);
1577+
assertValidAnimation(animations[0], 'removeClass', 'ng-invalid');
1578+
assertValidAnimation(animations[1], 'addClass', 'ng-valid');
1579+
assertValidAnimation(animations[2], 'removeClass', 'ng-invalid-custom-error');
1580+
assertValidAnimation(animations[3], 'addClass', 'ng-valid-custom-error');
1581+
}));
1582+
});

0 commit comments

Comments
 (0)
Please sign in to comment.