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
This commit is contained in:
committed by
Matias Niemelä
parent
8794a173f9
commit
33443966c8
@@ -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
|
||||
* 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
|
||||
* related scope, under this name.
|
||||
*/
|
||||
var formDirectiveFactory = function(isNgForm) {
|
||||
return ['$timeout', function($timeout) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -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('<form name="myForm">' +
|
||||
' <input type="text" ng-model="input" name="myInput" />' +
|
||||
'</form>');
|
||||
$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');
|
||||
}));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user