diff options
| author | Misko Hevery | 2012-03-13 13:17:39 -0700 | 
|---|---|---|
| committer | Misko Hevery | 2012-03-13 16:57:36 -0700 | 
| commit | d34f3bc7a61056a1f4aebb49d2475414fa16d5e4 (patch) | |
| tree | 0003c7f113c1c90475eea34f20a7f1061f764aab | |
| parent | 027801a00accbacefcb8fed059d09340ac5403ec (diff) | |
| download | angular.js-d34f3bc7a61056a1f4aebb49d2475414fa16d5e4.tar.bz2 | |
feat(form): publish validationErrorKeys as CSS
- The validationErrorKeys are now published as CSS for easy styling. The errorKeys should be in
 camelCase and the CSS will be in snake-case
| -rw-r--r-- | src/directive/form.js | 42 | ||||
| -rw-r--r-- | src/directive/input.js | 53 | ||||
| -rw-r--r-- | test/directive/formSpec.js | 42 | ||||
| -rw-r--r-- | test/directive/inputSpec.js | 39 | 
4 files changed, 116 insertions, 60 deletions
diff --git a/src/directive/form.js b/src/directive/form.js index 6bb1b4d6..47274589 100644 --- a/src/directive/form.js +++ b/src/directive/form.js @@ -35,6 +35,7 @@ FormController.$inject = ['name', '$element', '$attrs'];  function FormController(name, element, attrs) {    var form = this,        parentForm = element.parent().inheritedData('$formController') || nullFormCtrl, +      invalidCount = 0, // used to easily determine if we are valid        errors = form.$error = {};    // init state @@ -49,11 +50,27 @@ function FormController(name, element, attrs) {    parentForm.$addControl(form); +  // Setup initial state of the control +  element.addClass(PRISTINE_CLASS); +  toggleValidCss(true); + +  // 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); +  } + +  if (parentForm) { +    parentForm.$addControl(form); +  } +    form.$addControl = function(control) {      if (control.$name && !form.hasOwnProperty(control.$name)) {        form[control.$name] = control;      } -  } +  };    form.$removeControl = function(control) {      if (control.$name && form[control.$name] === control) { @@ -66,11 +83,15 @@ function FormController(name, element, attrs) {      if (isValid) {        cleanupControlErrors(errors[validationToken], validationToken, control); -      if (equals(errors, {})) { +      if (!invalidCount) { +        toggleValidCss(isValid);          form.$valid = true;          form.$invalid = false;        }      } else { +      if (!invalidCount) { +        toggleValidCss(isValid); +      }        addControlError(validationToken, control);        form.$valid = false; @@ -79,16 +100,19 @@ function FormController(name, element, attrs) {    };    form.$setDirty = function() { +    element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);      form.$dirty = true;      form.$pristine = false; -  } +  };    function cleanupControlErrors(queue, validationToken, control) {      if (queue) {        control = control || this; // so that we can be used in forEach;        arrayRemove(queue, control);        if (!queue.length) { -        delete errors[validationToken]; +        invalidCount--; +        errors[validationToken] = false; +        toggleValidCss(true, validationToken);          parentForm.$setValidity(validationToken, true, form);        }      } @@ -100,6 +124,8 @@ function FormController(name, element, attrs) {        if (includes(queue, control)) return;      } else {        errors[validationToken] = queue = []; +      invalidCount++; +      toggleValidCss(false, validationToken);        parentForm.$setValidity(validationToken, false, form);      }      queue.push(control); @@ -211,14 +237,6 @@ var formDirective = [function() {              if (!attr.action) event.preventDefault();            }); -          forEach(['valid', 'invalid', 'dirty', 'pristine'], function(name) { -            scope.$watch(function() { -              return controller['$' + name]; -            }, function(value) { -              formElement[value ? 'addClass' : 'removeClass']('ng-' + name); -            }); -          }); -            var parentFormCtrl = formElement.parent().inheritedData('$formController');            if (parentFormCtrl) {              formElement.bind('$destroy', function() { diff --git a/src/directive/input.js b/src/directive/input.js index a92ad306..740f3ba6 100644 --- a/src/directive/input.js +++ b/src/directive/input.js @@ -719,6 +719,10 @@ var inputDirective = [function() {    };  }]; +var VALID_CLASS = 'ng-valid', +    INVALID_CLASS = 'ng-invalid', +    PRISTINE_CLASS = 'ng-pristine', +    DIRTY_CLASS = 'ng-dirty';  /**   * @ngdoc object @@ -749,7 +753,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e    this.$parsers = [];    this.$formatters = [];    this.$viewChangeListeners = []; -  this.$error = {};    this.$pristine = true;    this.$dirty = false;    this.$valid = true; @@ -757,7 +760,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e    this.$render = noop;    this.$name = $attr.name; -  var parentForm = $element.inheritedData('$formController') || nullFormCtrl; +  var parentForm = $element.inheritedData('$formController') || nullFormCtrl, +      invalidCount = 0, // used to easily determine if we are valid +      $error = this.$error = {}; // keep invalid keys here + + +  // Setup initial state of the control +  $element.addClass(PRISTINE_CLASS); +  toggleValidCss(true); + +  // 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); +  }    /**     * @ngdoc function @@ -770,22 +788,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e     *     * This method should be called by validators - i.e. the parser or formatter functions.     * -   * @param {string} validationErrorKey Name of the validator. +   * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign +   *        to `$error[validationErrorKey]=isValid` so that it is available for data-binding. +   *        The `validationErrorKey` should be in camelCase and will get converted into dash-case +   *        for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` +   *        class and can be bound to as  `{{someForm.someControl.$error.myError}}` .     * @param {boolean} isValid Whether the current state is valid (true) or invalid (false).     */    this.$setValidity = function(validationErrorKey, isValid) { - -    if (!isValid && this.$error[validationErrorKey]) return; -    if (isValid && !this.$error[validationErrorKey]) return; +    if ($error[validationErrorKey] === !isValid) return;      if (isValid) { -      delete this.$error[validationErrorKey]; -      if (equals(this.$error, {})) { +      if ($error[validationErrorKey]) invalidCount--; +      $error[validationErrorKey] = false; +      toggleValidCss(isValid); +      if (!invalidCount) { +        toggleValidCss(isValid, validationErrorKey);          this.$valid = true;          this.$invalid = false;        }      } else { -      this.$error[validationErrorKey] = true; +      if (!$error[validationErrorKey]) invalidCount++; +      $error[validationErrorKey] = true; +      toggleValidCss(isValid) +      toggleValidCss(isValid, validationErrorKey);        this.$invalid = true;        this.$valid = false;      } @@ -818,6 +844,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e      if (this.$pristine) {        this.$dirty = true;        this.$pristine = false; +      $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);        parentForm.$setDirty();      } @@ -910,14 +937,6 @@ var ngModelDirective = [function() {        formCtrl.$addControl(modelCtrl); -      forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) { -        scope.$watch(function() { -          return modelCtrl['$' + name]; -        }, function(value) { -          element[value ? 'addClass' : 'removeClass']('ng-' + name); -        }); -      }); -        element.bind('$destroy', function() {          formCtrl.$removeControl(modelCtrl);        }); diff --git a/test/directive/formSpec.js b/test/directive/formSpec.js index fbcf4aec..588b9f49 100644 --- a/test/directive/formSpec.js +++ b/test/directive/formSpec.js @@ -43,7 +43,7 @@ describe('form', function() {      expect(form.$error.required).toEqual([control]);      doc.find('input').remove(); -    expect(form.$error.required).toBeUndefined(); +    expect(form.$error.required).toBe(false);      expect(form.alias).toBeUndefined();    }); @@ -124,8 +124,8 @@ describe('form', function() {      expect(scope.firstName).toBe('val1');      expect(scope.lastName).toBe('val2'); -    expect(scope.formA.$error.required).toBeUndefined(); -    expect(scope.formB.$error.required).toBeUndefined(); +    expect(scope.formA.$error.required).toBe(false); +    expect(scope.formB.$error.required).toBe(false);    }); @@ -169,8 +169,8 @@ describe('form', function() {        expect(child.$error.MyError).toEqual([inputB]);        inputB.$setValidity('MyError', true); -      expect(parent.$error.MyError).toBeUndefined(); -      expect(child.$error.MyError).toBeUndefined(); +      expect(parent.$error.MyError).toBe(false); +      expect(child.$error.MyError).toBe(false);      }); @@ -192,7 +192,7 @@ describe('form', function() {        expect(parent.child).toBeUndefined();        expect(scope.child).toBeUndefined(); -      expect(parent.$error.required).toBeUndefined(); +      expect(parent.$error.required).toBe(false);      }); @@ -223,8 +223,8 @@ describe('form', function() {        expect(parent.$error.myRule).toEqual([child]);        input.$setValidity('myRule', true); -      expect(parent.$error.myRule).toBeUndefined(); -      expect(child.$error.myRule).toBeUndefined(); +      expect(parent.$error.myRule).toBe(false); +      expect(child.$error.myRule).toBe(false);      });    }) @@ -244,20 +244,30 @@ describe('form', function() {      it('should have ng-valid/ng-invalid css class', function() {        expect(doc).toBeValid(); -      control.$setValidity('ERROR', false); -      scope.$apply(); +      control.$setValidity('error', false);        expect(doc).toBeInvalid(); +      expect(doc.hasClass('ng-valid-error')).toBe(false); +      expect(doc.hasClass('ng-invalid-error')).toBe(true); -      control.$setValidity('ANOTHER', false); -      scope.$apply(); +      control.$setValidity('another', false); +      expect(doc.hasClass('ng-valid-error')).toBe(false); +      expect(doc.hasClass('ng-invalid-error')).toBe(true); +      expect(doc.hasClass('ng-valid-another')).toBe(false); +      expect(doc.hasClass('ng-invalid-another')).toBe(true); -      control.$setValidity('ERROR', true); -      scope.$apply(); +      control.$setValidity('error', true);        expect(doc).toBeInvalid(); +      expect(doc.hasClass('ng-valid-error')).toBe(true); +      expect(doc.hasClass('ng-invalid-error')).toBe(false); +      expect(doc.hasClass('ng-valid-another')).toBe(false); +      expect(doc.hasClass('ng-invalid-another')).toBe(true); -      control.$setValidity('ANOTHER', true); -      scope.$apply(); +      control.$setValidity('another', true);        expect(doc).toBeValid(); +      expect(doc.hasClass('ng-valid-error')).toBe(true); +      expect(doc.hasClass('ng-invalid-error')).toBe(false); +      expect(doc.hasClass('ng-valid-another')).toBe(true); +      expect(doc.hasClass('ng-invalid-another')).toBe(false);      }); diff --git a/test/directive/inputSpec.js b/test/directive/inputSpec.js index 4bdba36a..a9aafd04 100644 --- a/test/directive/inputSpec.js +++ b/test/directive/inputSpec.js @@ -66,38 +66,39 @@ describe('NgModelController', function() {        expect(ctrl.$error.required).toBe(true);        ctrl.$setValidity('required', true); -      expect(ctrl.$error.required).toBeUndefined(); +      expect(ctrl.$error.required).toBe(false);      });      it('should set valid/invalid', function() { -      ctrl.$setValidity('FIRST', false); +      ctrl.$setValidity('first', false);        expect(ctrl.$valid).toBe(false);        expect(ctrl.$invalid).toBe(true); -      ctrl.$setValidity('SECOND', false); +      ctrl.$setValidity('second', false);        expect(ctrl.$valid).toBe(false);        expect(ctrl.$invalid).toBe(true); -      ctrl.$setValidity('SECOND', true); +      ctrl.$setValidity('second', true);        expect(ctrl.$valid).toBe(false);        expect(ctrl.$invalid).toBe(true); -      ctrl.$setValidity('FIRST', true); +      ctrl.$setValidity('first', true);        expect(ctrl.$valid).toBe(true);        expect(ctrl.$invalid).toBe(false);      });      it('should emit $valid only when $invalid', function() { -      ctrl.$setValidity('ERROR', true); -      expect(parentFormCtrl.$setValidity).not.toHaveBeenCalled(); +      ctrl.$setValidity('error', true); +      expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', true, ctrl); +      parentFormCtrl.$setValidity.reset(); -      ctrl.$setValidity('ERROR', false); -      expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('ERROR', false, ctrl); +      ctrl.$setValidity('error', false); +      expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', false, ctrl);        parentFormCtrl.$setValidity.reset(); -      ctrl.$setValidity('ERROR', true); -      expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('ERROR', true, ctrl); +      ctrl.$setValidity('error', true); +      expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', true, ctrl);      });    }); @@ -152,7 +153,7 @@ describe('NgModelController', function() {        ctrl.$parsers.push(function() {return undefined;});        expect(ctrl.$modelValue).toBe('aaaa');        ctrl.$setViewValue('bbbb'); -      expect(ctrl.$modelValue).toBeUndefined; +      expect(ctrl.$modelValue).toBeUndefined();      }); @@ -241,22 +242,30 @@ describe('ng-model', function() {      $rootScope.$digest();      expect(element).toBeValid();      expect(element).toBePristine(); +    expect(element.hasClass('ng-valid-email')).toBe(true); +    expect(element.hasClass('ng-invalid-email')).toBe(false);      $rootScope.$apply(function() {        $rootScope.value = 'invalid-email';      });      expect(element).toBeInvalid();      expect(element).toBePristine(); +    expect(element.hasClass('ng-valid-email')).toBe(false); +    expect(element.hasClass('ng-invalid-email')).toBe(true);      element.val('invalid-again');      browserTrigger(element, 'blur');      expect(element).toBeInvalid();      expect(element).toBeDirty(); +    expect(element.hasClass('ng-valid-email')).toBe(false); +    expect(element.hasClass('ng-invalid-email')).toBe(true);      element.val('vojta@google.com');      browserTrigger(element, 'blur');      expect(element).toBeValid();      expect(element).toBeDirty(); +    expect(element.hasClass('ng-valid-email')).toBe(true); +    expect(element.hasClass('ng-invalid-email')).toBe(false);      dealoc(element);    })); @@ -305,7 +314,7 @@ describe('input', function() {      expect(scope.form.$error.required.length).toBe(1);      inputElm.remove(); -    expect(scope.form.$error.required).toBeUndefined(); +    expect(scope.form.$error.required).toBe(false);    }); @@ -605,7 +614,7 @@ describe('input', function() {        expect(scope.email).toBe('vojta@google.com');        expect(inputElm).toBeValid(); -      expect(widget.$error.email).toBeUndefined(); +      expect(widget.$error.email).toBe(false);        changeInputValueTo('invalid@');        expect(scope.email).toBeUndefined(); @@ -633,7 +642,7 @@ describe('input', function() {        changeInputValueTo('http://www.something.com');        expect(scope.url).toBe('http://www.something.com');        expect(inputElm).toBeValid(); -      expect(widget.$error.url).toBeUndefined(); +      expect(widget.$error.url).toBe(false);        changeInputValueTo('invalid.com');        expect(scope.url).toBeUndefined();  | 
