Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 6046e14

Browse files
committed
refactor(ngModelController,formController): centralize and simplify logic
The previous logic for async validation in `ngModelController` and `formController` was not maintainable: - control logic is in multiple parts, e.g. `ctrl.$setValidity` waits for end of promises and continuous the control flow for async validation - logic for updating the flags `ctrl.$error`, `ctrl.$pending`, `ctrl.$valid` is super complicated, especially in `formController` This refactoring makes the following changes: - simplify async validation: centralize control logic into one method in `ngModelController`: * remove counters `invalidCount` and `pendingCount` * use a flag `currentValidationRunId` to separate async validator runs from each other * use `$q.all` to determine when all async validators are done - centralize way how `ctrl.$modelValue` and `ctrl.$invalidModelValue` is updated - simplify `ngModelController/formCtrl.$setValidity` and merge `$$setPending/$$clearControlValidity/$$clearValidity/$$clearPending` into one method, that is used by `ngModelController` AND `formController` * remove diff calculation, always calculate the correct state anew, only cache the css classes that have been set to not trigger too many css animations. * remove fields from `ctrl.$error` that are valid and add private `ctrl.$$success`: allows to correctly separate states for valid, invalid, skipped and pending, especially transitively across parent forms. - fix bug in `ngModelController`: * only read out `input.validity.badInput`, but not `input.validity.typeMismatch`, to determine parser error: We still want our `email` validator to run event when the model is validated. - fix bugs in tests that were found as the logic is now consistent between `ngModelController` and `formController` BREAKING CHANGE: - `ctrl.$error` does no more contain entries for validators that were successful. - `ctrl.$setValidity` now differentiates between `true`, `false`, `undefined` and `null`, instead of previously only truthy vs falsy. Closes #8941
1 parent 2a5af50 commit 6046e14

File tree

4 files changed

+369
-340
lines changed

4 files changed

+369
-340
lines changed

src/ng/directive/form.js

+38-103
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

3-
/* global -nullFormCtrl, -SUBMITTED_CLASS */
3+
/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true
4+
*/
45
var nullFormCtrl = {
56
$addControl: noop,
67
$removeControl: noop,
@@ -23,12 +24,11 @@ SUBMITTED_CLASS = 'ng-submitted';
2324
* @property {boolean} $invalid True if at least one containing control or form is invalid.
2425
* @property {boolean} $submitted True if user has submitted the form even if its invalid.
2526
*
26-
* @property {Object} $error Is an object hash, containing references to all invalid controls or
27-
* forms, where:
27+
* @property {Object} $error Is an object hash, containing references to controls or
28+
* forms with failing validators, where:
2829
*
2930
* - keys are validation tokens (error names),
30-
* - values are arrays of controls or forms that are invalid for given error name.
31-
*
31+
* - values are arrays of controls or forms that have a failing validator for given error name.
3232
*
3333
* Built-in validation tokens:
3434
*
@@ -55,12 +55,12 @@ FormController.$inject = ['$element', '$attrs', '$scope', '$animate'];
5555
function FormController(element, attrs, $scope, $animate) {
5656
var form = this,
5757
parentForm = element.parent().controller('form') || nullFormCtrl,
58-
invalidCount = 0, // used to easily determine if we are valid
59-
pendingCount = 0,
60-
controls = [],
61-
errors = form.$error = {};
58+
controls = [];
6259

6360
// init state
61+
form.$error = {};
62+
form.$$success = {};
63+
form.$pending = undefined;
6464
form.$name = attrs.name || attrs.ngForm;
6565
form.$dirty = false;
6666
form.$pristine = true;
@@ -72,14 +72,6 @@ function FormController(element, attrs, $scope, $animate) {
7272

7373
// Setup initial state of the control
7474
element.addClass(PRISTINE_CLASS);
75-
toggleValidCss(true);
76-
77-
// convenience method for easy toggling of classes
78-
function toggleValidCss(isValid, validationErrorKey) {
79-
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
80-
$animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey);
81-
$animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
82-
}
8375

8476
/**
8577
* @ngdoc method
@@ -148,34 +140,16 @@ function FormController(element, attrs, $scope, $animate) {
148140
if (control.$name && form[control.$name] === control) {
149141
delete form[control.$name];
150142
}
143+
forEach(form.$pending, function(value, name) {
144+
form.$setValidity(name, null, control);
145+
});
146+
forEach(form.$error, function(value, name) {
147+
form.$setValidity(name, null, control);
148+
});
151149

152-
form.$$clearControlValidity(control);
153150
arrayRemove(controls, control);
154151
};
155152

156-
form.$$clearControlValidity = function(control) {
157-
forEach(form.$pending, clear);
158-
forEach(errors, clear);
159-
160-
function clear(queue, validationToken) {
161-
form.$setValidity(validationToken, true, control);
162-
}
163-
};
164-
165-
form.$$setPending = function(validationToken, control) {
166-
var pending = form.$pending && form.$pending[validationToken];
167-
168-
if (!pending || !includes(pending, control)) {
169-
pendingCount++;
170-
form.$valid = form.$invalid = undefined;
171-
form.$pending = form.$pending || {};
172-
if (!pending) {
173-
pending = form.$pending[validationToken] = [];
174-
}
175-
pending.push(control);
176-
parentForm.$$setPending(validationToken, form);
177-
}
178-
};
179153

180154
/**
181155
* @ngdoc method
@@ -186,72 +160,33 @@ function FormController(element, attrs, $scope, $animate) {
186160
*
187161
* This method will also propagate to parent forms.
188162
*/
189-
form.$setValidity = function(validationToken, isValid, control) {
190-
var queue = errors[validationToken];
191-
var pendingChange, pending = form.$pending && form.$pending[validationToken];
192-
193-
if (pending) {
194-
pendingChange = pending.indexOf(control) >= 0;
195-
if (pendingChange) {
196-
arrayRemove(pending, control);
197-
pendingCount--;
198-
199-
if (pending.length === 0) {
200-
delete form.$pending[validationToken];
201-
}
202-
}
203-
}
204-
205-
var pendingNoMore = form.$pending && pendingCount === 0;
206-
if (pendingNoMore) {
207-
form.$pending = undefined;
208-
}
209-
210-
if (isValid) {
211-
if (queue || pendingChange) {
212-
if (queue) {
213-
arrayRemove(queue, control);
214-
}
215-
if (!queue || !queue.length) {
216-
if (errors[validationToken]) {
217-
invalidCount--;
218-
}
219-
if (!invalidCount) {
220-
if (!form.$pending) {
221-
toggleValidCss(isValid);
222-
form.$valid = true;
223-
form.$invalid = false;
224-
}
225-
} else if(pendingNoMore) {
226-
toggleValidCss(false);
227-
form.$valid = false;
228-
form.$invalid = true;
229-
}
230-
errors[validationToken] = false;
231-
toggleValidCss(true, validationToken);
232-
parentForm.$setValidity(validationToken, true, form);
163+
addSetValidityMethod({
164+
ctrl: this,
165+
$element: element,
166+
set: function(object, property, control) {
167+
var list = object[property];
168+
if (!list) {
169+
object[property] = [control];
170+
} else {
171+
var index = list.indexOf(control);
172+
if (index === -1) {
173+
list.push(control);
233174
}
234175
}
235-
} else {
236-
if (!form.$pending) {
237-
form.$valid = false;
238-
form.$invalid = true;
176+
},
177+
unset: function(object, property, control) {
178+
var list = object[property];
179+
if (!list) {
180+
return;
239181
}
240-
241-
if (!invalidCount) {
242-
toggleValidCss(isValid);
182+
arrayRemove(list, control);
183+
if (list.length === 0) {
184+
delete object[property];
243185
}
244-
if (queue) {
245-
if (includes(queue, control)) return;
246-
} else {
247-
errors[validationToken] = queue = [];
248-
invalidCount++;
249-
toggleValidCss(false, validationToken);
250-
parentForm.$setValidity(validationToken, false, form);
251-
}
252-
queue.push(control);
253-
}
254-
};
186+
},
187+
parentForm: parentForm,
188+
$animate: $animate
189+
});
255190

256191
/**
257192
* @ngdoc method

0 commit comments

Comments
 (0)