Skip to content

Commit

Permalink
feat(form): publish validationErrorKeys as CSS
Browse files Browse the repository at this point in the history
- The validationErrorKeys are now published as CSS for easy styling. The errorKeys should be in
 camelCase and the CSS will be in snake-case
  • Loading branch information
mhevery committed Mar 13, 2012
1 parent 027801a commit d34f3bc
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 60 deletions.
42 changes: 30 additions & 12 deletions src/directive/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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);
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -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() {
Expand Down
53 changes: 36 additions & 17 deletions src/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -749,15 +753,29 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
this.$parsers = [];
this.$formatters = [];
this.$viewChangeListeners = [];
this.$error = {};
this.$pristine = true;
this.$dirty = false;
this.$valid = true;
this.$invalid = false;
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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
});
Expand Down
42 changes: 26 additions & 16 deletions test/directive/formSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

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


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


Expand All @@ -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);
});


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

Expand All @@ -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);
});


Expand Down
Loading

0 comments on commit d34f3bc

Please sign in to comment.