Skip to content

Commit

Permalink
feat($animate): animate dirty, pristine, valid, invalid for form/fields
Browse files Browse the repository at this point in the history
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
  • Loading branch information
eunomie authored and matsko committed Feb 28, 2014
1 parent 8794a17 commit 5f4d8ea
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 17 deletions.
58 changes: 49 additions & 9 deletions src/ng/directive/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -279,8 +280,28 @@ function FormController(element, attrs) {
* hitting enter in any of the input fields will trigger the click handler on the *first* button or
* input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`)
*
* @param {string=} name Name of the form. If specified, the form controller will be published into
* related scope, under this name.
* ## A note about animations with `ngForm`
*
* Animations in ngForm work with the pristine, dirty, invalid and valid events that are triggered when
* the values of form change. This system works like the animation system present with ngClass.
*
* <pre>
* //
* //a working example can be found at the bottom of this page
* //
* .my-element.ng-dirty-add {
* transition:0.5s linear all;
* background: red;
* }
* .my-element.ng-dirty {
* background: white;
* }
*
* .my-element.ng-dirty-add { ... }
* .my-element.ng-dirty-add.ng-dirty-add-active { ... }
* .my-element.ng-dirty-remove { ... }
* .my-element.ng-dirty-remove.ng-dirty-remove-active { ... }
* </pre>
*
* @example
<example>
Expand All @@ -290,6 +311,16 @@ function FormController(element, attrs) {
$scope.userType = 'guest';
}
</script>
<style>
form.ng-dirty-add {
-webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
background: orange;
}
form.ng-dirty {
background: transparent;
}
</style>
<form name="myForm" ng-controller="Ctrl">
userType: <input name="input" ng-model="userType" required>
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
Expand Down Expand Up @@ -322,6 +353,15 @@ 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.
*
* @animations
* removeClass .ng-dirty and addClass .ng-pristine: happens just after form became pristine
* removeClass .ng-pristine and addClass .ng-dirty: happens just after form became dirty
* removeClass .ng-invalid and addClass .ng-valid: happens just after form became valid
* removeClass .ng-valid and addClass .ng-invalid: happens just after form became invalid
*/
var formDirectiveFactory = function(isNgForm) {
return ['$timeout', function($timeout) {
Expand Down
71 changes: 63 additions & 8 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
};

/**
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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
Expand All @@ -1248,6 +1249,60 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* - {@link ng.directive:select select}
* - {@link ng.directive:textarea textarea}
*
* ## A note about animations with `NgModel`
*
* Animations work with the pristine, dirty, invalid and valid events that are triggered when
* the values of input change. This system works like the animation system present with ngClass.
*
* <pre>
* //
* //a working example can be found at the bottom of this page
* //
* .my-element.ng-dirty-add {
* transition:0.5s linear all;
* background: red;
* }
* .my-element.ng-dirty {
* background: white;
* }
*
* .my-element.ng-dirty-add { ... }
* .my-element.ng-dirty-add.ng-dirty-add-active { ... }
* .my-element.ng-dirty-remove { ... }
* .my-element.ng-dirty-remove.ng-dirty-remove-active { ... }
* </pre>
*
* @animations
* removeClass .ng-dirty and addClass .ng-pristine: happens just after input became pristine
* removeClass .ng-pristine and addClass .ng-dirty: happens just after input became dirty
* removeClass .ng-invalid and addClass .ng-valid: happens just after input became valid
* removeClass .ng-valid and addClass .ng-invalid: happens just after input became invalid
*
* @example
* <doc:example>
<doc:source>
<script>
function Ctrl($scope) {
$scope.val = '1';
}
</script>
<style>
input.ng-invalid-pattern-add {
-webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
background: red;
}
input.ng-invalid {
background: white;
}
</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"/>
</form>
</doc:source>
* </doc:example>
*/
var ngModelDirective = function() {
return {
Expand Down
2 changes: 2 additions & 0 deletions src/ngAnimate/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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} | dirty, pristine, valid and invalid |
* | {@link ng.directive:ngModel#usage_animations ngModel} | dirty, pristine, valid and invalid |
*
* You can find out more information about animations upon visiting each directive page.
*
Expand Down
80 changes: 80 additions & 0 deletions test/ng/directive/formSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}));
});
Loading

0 comments on commit 5f4d8ea

Please sign in to comment.