Skip to content

Commit d34f3bc

Browse files
committed
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
1 parent 027801a commit d34f3bc

File tree

4 files changed

+116
-60
lines changed

4 files changed

+116
-60
lines changed

src/directive/form.js

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ FormController.$inject = ['name', '$element', '$attrs'];
3535
function FormController(name, element, attrs) {
3636
var form = this,
3737
parentForm = element.parent().inheritedData('$formController') || nullFormCtrl,
38+
invalidCount = 0, // used to easily determine if we are valid
3839
errors = form.$error = {};
3940

4041
// init state
@@ -49,11 +50,27 @@ function FormController(name, element, attrs) {
4950

5051
parentForm.$addControl(form);
5152

53+
// Setup initial state of the control
54+
element.addClass(PRISTINE_CLASS);
55+
toggleValidCss(true);
56+
57+
// convenience method for easy toggling of classes
58+
function toggleValidCss(isValid, validationErrorKey) {
59+
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
60+
element.
61+
removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
62+
addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
63+
}
64+
65+
if (parentForm) {
66+
parentForm.$addControl(form);
67+
}
68+
5269
form.$addControl = function(control) {
5370
if (control.$name && !form.hasOwnProperty(control.$name)) {
5471
form[control.$name] = control;
5572
}
56-
}
73+
};
5774

5875
form.$removeControl = function(control) {
5976
if (control.$name && form[control.$name] === control) {
@@ -66,11 +83,15 @@ function FormController(name, element, attrs) {
6683
if (isValid) {
6784
cleanupControlErrors(errors[validationToken], validationToken, control);
6885

69-
if (equals(errors, {})) {
86+
if (!invalidCount) {
87+
toggleValidCss(isValid);
7088
form.$valid = true;
7189
form.$invalid = false;
7290
}
7391
} else {
92+
if (!invalidCount) {
93+
toggleValidCss(isValid);
94+
}
7495
addControlError(validationToken, control);
7596

7697
form.$valid = false;
@@ -79,16 +100,19 @@ function FormController(name, element, attrs) {
79100
};
80101

81102
form.$setDirty = function() {
103+
element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
82104
form.$dirty = true;
83105
form.$pristine = false;
84-
}
106+
};
85107

86108
function cleanupControlErrors(queue, validationToken, control) {
87109
if (queue) {
88110
control = control || this; // so that we can be used in forEach;
89111
arrayRemove(queue, control);
90112
if (!queue.length) {
91-
delete errors[validationToken];
113+
invalidCount--;
114+
errors[validationToken] = false;
115+
toggleValidCss(true, validationToken);
92116
parentForm.$setValidity(validationToken, true, form);
93117
}
94118
}
@@ -100,6 +124,8 @@ function FormController(name, element, attrs) {
100124
if (includes(queue, control)) return;
101125
} else {
102126
errors[validationToken] = queue = [];
127+
invalidCount++;
128+
toggleValidCss(false, validationToken);
103129
parentForm.$setValidity(validationToken, false, form);
104130
}
105131
queue.push(control);
@@ -211,14 +237,6 @@ var formDirective = [function() {
211237
if (!attr.action) event.preventDefault();
212238
});
213239

214-
forEach(['valid', 'invalid', 'dirty', 'pristine'], function(name) {
215-
scope.$watch(function() {
216-
return controller['$' + name];
217-
}, function(value) {
218-
formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
219-
});
220-
});
221-
222240
var parentFormCtrl = formElement.parent().inheritedData('$formController');
223241
if (parentFormCtrl) {
224242
formElement.bind('$destroy', function() {

src/directive/input.js

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,10 @@ var inputDirective = [function() {
719719
};
720720
}];
721721

722+
var VALID_CLASS = 'ng-valid',
723+
INVALID_CLASS = 'ng-invalid',
724+
PRISTINE_CLASS = 'ng-pristine',
725+
DIRTY_CLASS = 'ng-dirty';
722726

723727
/**
724728
* @ngdoc object
@@ -749,15 +753,29 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
749753
this.$parsers = [];
750754
this.$formatters = [];
751755
this.$viewChangeListeners = [];
752-
this.$error = {};
753756
this.$pristine = true;
754757
this.$dirty = false;
755758
this.$valid = true;
756759
this.$invalid = false;
757760
this.$render = noop;
758761
this.$name = $attr.name;
759762

760-
var parentForm = $element.inheritedData('$formController') || nullFormCtrl;
763+
var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
764+
invalidCount = 0, // used to easily determine if we are valid
765+
$error = this.$error = {}; // keep invalid keys here
766+
767+
768+
// Setup initial state of the control
769+
$element.addClass(PRISTINE_CLASS);
770+
toggleValidCss(true);
771+
772+
// convenience method for easy toggling of classes
773+
function toggleValidCss(isValid, validationErrorKey) {
774+
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
775+
$element.
776+
removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
777+
addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
778+
}
761779

762780
/**
763781
* @ngdoc function
@@ -770,22 +788,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
770788
*
771789
* This method should be called by validators - i.e. the parser or formatter functions.
772790
*
773-
* @param {string} validationErrorKey Name of the validator.
791+
* @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
792+
* to `$error[validationErrorKey]=isValid` so that it is available for data-binding.
793+
* The `validationErrorKey` should be in camelCase and will get converted into dash-case
794+
* for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
795+
* class and can be bound to as `{{someForm.someControl.$error.myError}}` .
774796
* @param {boolean} isValid Whether the current state is valid (true) or invalid (false).
775797
*/
776798
this.$setValidity = function(validationErrorKey, isValid) {
777-
778-
if (!isValid && this.$error[validationErrorKey]) return;
779-
if (isValid && !this.$error[validationErrorKey]) return;
799+
if ($error[validationErrorKey] === !isValid) return;
780800

781801
if (isValid) {
782-
delete this.$error[validationErrorKey];
783-
if (equals(this.$error, {})) {
802+
if ($error[validationErrorKey]) invalidCount--;
803+
$error[validationErrorKey] = false;
804+
toggleValidCss(isValid);
805+
if (!invalidCount) {
806+
toggleValidCss(isValid, validationErrorKey);
784807
this.$valid = true;
785808
this.$invalid = false;
786809
}
787810
} else {
788-
this.$error[validationErrorKey] = true;
811+
if (!$error[validationErrorKey]) invalidCount++;
812+
$error[validationErrorKey] = true;
813+
toggleValidCss(isValid)
814+
toggleValidCss(isValid, validationErrorKey);
789815
this.$invalid = true;
790816
this.$valid = false;
791817
}
@@ -818,6 +844,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
818844
if (this.$pristine) {
819845
this.$dirty = true;
820846
this.$pristine = false;
847+
$element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
821848
parentForm.$setDirty();
822849
}
823850

@@ -910,14 +937,6 @@ var ngModelDirective = [function() {
910937

911938
formCtrl.$addControl(modelCtrl);
912939

913-
forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) {
914-
scope.$watch(function() {
915-
return modelCtrl['$' + name];
916-
}, function(value) {
917-
element[value ? 'addClass' : 'removeClass']('ng-' + name);
918-
});
919-
});
920-
921940
element.bind('$destroy', function() {
922941
formCtrl.$removeControl(modelCtrl);
923942
});

test/directive/formSpec.js

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('form', function() {
4343
expect(form.$error.required).toEqual([control]);
4444

4545
doc.find('input').remove();
46-
expect(form.$error.required).toBeUndefined();
46+
expect(form.$error.required).toBe(false);
4747
expect(form.alias).toBeUndefined();
4848
});
4949

@@ -124,8 +124,8 @@ describe('form', function() {
124124
expect(scope.firstName).toBe('val1');
125125
expect(scope.lastName).toBe('val2');
126126

127-
expect(scope.formA.$error.required).toBeUndefined();
128-
expect(scope.formB.$error.required).toBeUndefined();
127+
expect(scope.formA.$error.required).toBe(false);
128+
expect(scope.formB.$error.required).toBe(false);
129129
});
130130

131131

@@ -169,8 +169,8 @@ describe('form', function() {
169169
expect(child.$error.MyError).toEqual([inputB]);
170170

171171
inputB.$setValidity('MyError', true);
172-
expect(parent.$error.MyError).toBeUndefined();
173-
expect(child.$error.MyError).toBeUndefined();
172+
expect(parent.$error.MyError).toBe(false);
173+
expect(child.$error.MyError).toBe(false);
174174
});
175175

176176

@@ -192,7 +192,7 @@ describe('form', function() {
192192

193193
expect(parent.child).toBeUndefined();
194194
expect(scope.child).toBeUndefined();
195-
expect(parent.$error.required).toBeUndefined();
195+
expect(parent.$error.required).toBe(false);
196196
});
197197

198198

@@ -223,8 +223,8 @@ describe('form', function() {
223223
expect(parent.$error.myRule).toEqual([child]);
224224

225225
input.$setValidity('myRule', true);
226-
expect(parent.$error.myRule).toBeUndefined();
227-
expect(child.$error.myRule).toBeUndefined();
226+
expect(parent.$error.myRule).toBe(false);
227+
expect(child.$error.myRule).toBe(false);
228228
});
229229
})
230230

@@ -244,20 +244,30 @@ describe('form', function() {
244244
it('should have ng-valid/ng-invalid css class', function() {
245245
expect(doc).toBeValid();
246246

247-
control.$setValidity('ERROR', false);
248-
scope.$apply();
247+
control.$setValidity('error', false);
249248
expect(doc).toBeInvalid();
249+
expect(doc.hasClass('ng-valid-error')).toBe(false);
250+
expect(doc.hasClass('ng-invalid-error')).toBe(true);
250251

251-
control.$setValidity('ANOTHER', false);
252-
scope.$apply();
252+
control.$setValidity('another', false);
253+
expect(doc.hasClass('ng-valid-error')).toBe(false);
254+
expect(doc.hasClass('ng-invalid-error')).toBe(true);
255+
expect(doc.hasClass('ng-valid-another')).toBe(false);
256+
expect(doc.hasClass('ng-invalid-another')).toBe(true);
253257

254-
control.$setValidity('ERROR', true);
255-
scope.$apply();
258+
control.$setValidity('error', true);
256259
expect(doc).toBeInvalid();
260+
expect(doc.hasClass('ng-valid-error')).toBe(true);
261+
expect(doc.hasClass('ng-invalid-error')).toBe(false);
262+
expect(doc.hasClass('ng-valid-another')).toBe(false);
263+
expect(doc.hasClass('ng-invalid-another')).toBe(true);
257264

258-
control.$setValidity('ANOTHER', true);
259-
scope.$apply();
265+
control.$setValidity('another', true);
260266
expect(doc).toBeValid();
267+
expect(doc.hasClass('ng-valid-error')).toBe(true);
268+
expect(doc.hasClass('ng-invalid-error')).toBe(false);
269+
expect(doc.hasClass('ng-valid-another')).toBe(true);
270+
expect(doc.hasClass('ng-invalid-another')).toBe(false);
261271
});
262272

263273

0 commit comments

Comments
 (0)