mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-19 08:00:23 +00:00
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
This commit is contained in:
parent
027801a00a
commit
d34f3bc7a6
4 changed files with 117 additions and 61 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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', false);
|
||||
expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('ERROR', false, ctrl);
|
||||
ctrl.$setValidity('error', true);
|
||||
expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', true, ctrl);
|
||||
parentFormCtrl.$setValidity.reset();
|
||||
ctrl.$setValidity('ERROR', true);
|
||||
expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('ERROR', true, 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue