aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMisko Hevery2012-03-13 13:17:39 -0700
committerMisko Hevery2012-03-13 16:57:36 -0700
commitd34f3bc7a61056a1f4aebb49d2475414fa16d5e4 (patch)
tree0003c7f113c1c90475eea34f20a7f1061f764aab
parent027801a00accbacefcb8fed059d09340ac5403ec (diff)
downloadangular.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.js42
-rw-r--r--src/directive/input.js53
-rw-r--r--test/directive/formSpec.js42
-rw-r--r--test/directive/inputSpec.js39
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();