diff options
| author | Yves Brissaud | 2013-12-10 21:49:31 +0100 | 
|---|---|---|
| committer | Matias Niemelä | 2014-02-28 01:01:34 -0500 | 
| commit | 33443966c8e8cac85a863bb181d4a4aff00baab4 (patch) | |
| tree | 2d811beee0b11ef997e084226343c7bc552207c7 | |
| parent | 8794a173f9c175df2343245e71ee9a137f5bc66a (diff) | |
| download | angular.js-33443966c8e8cac85a863bb181d4a4aff00baab4.tar.bz2 | |
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
| -rw-r--r-- | src/ng/directive/form.js | 57 | ||||
| -rw-r--r-- | src/ng/directive/input.js | 78 | ||||
| -rw-r--r-- | src/ngAnimate/animate.js | 2 | ||||
| -rw-r--r-- | test/ng/directive/formSpec.js | 80 | ||||
| -rw-r--r-- | test/ng/directive/inputSpec.js | 98 | 
5 files changed, 298 insertions, 17 deletions
| diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index c5e39cde..4e6ec20d 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -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) { diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index d6ee26be..f584eda4 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -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 { diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index b4baf3e2..5f2d4401 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -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.   * diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index dde6f0a0..b55d1f8d 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -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'); +  })); +}); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index b9f737ac..e3e50e02 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -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'); +  })); +}); | 
