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

Commit

Permalink
refactor(ngModelController,formController): centralize and simplify l…
Browse files Browse the repository at this point in the history
…ogic

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 `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. Instead, they are now saved in `ctrl.$success`.
- `ctrl.$setValidity` now differentiates between `true`, `false`,
  `undefined` and `null`, instead of previously only truthy vs falsy.
  • Loading branch information
tbosch committed Sep 5, 2014
1 parent e322cd9 commit 09c8079
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 308 deletions.
134 changes: 35 additions & 99 deletions src/ng/directive/form.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

/* global -nullFormCtrl, -SUBMITTED_CLASS */
/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true
*/
var nullFormCtrl = {
$addControl: noop,
$removeControl: noop,
Expand Down Expand Up @@ -55,12 +56,12 @@ FormController.$inject = ['$element', '$attrs', '$scope', '$animate'];
function FormController(element, attrs, $scope, $animate) {
var form = this,
parentForm = element.parent().controller('form') || nullFormCtrl,
invalidCount = 0, // used to easily determine if we are valid
pendingCount = 0,
controls = [],
errors = form.$error = {};
controls = [];

// init state
form.$error = {};
form.$success = {};
form.$pending = undefined;
form.$name = attrs.name || attrs.ngForm;
form.$dirty = false;
form.$pristine = true;
Expand All @@ -72,14 +73,6 @@ function FormController(element, attrs, $scope, $animate) {

// 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, '-') : '';
$animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey);
$animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
}

/**
* @ngdoc method
Expand Down Expand Up @@ -148,34 +141,16 @@ function FormController(element, attrs, $scope, $animate) {
if (control.$name && form[control.$name] === control) {
delete form[control.$name];
}
forEach(form.$pending, function(value, name) {
form.$setValidity(name, null, control);
});
forEach(form.$error, function(value, name) {
form.$setValidity(name, null, control);
});

form.$$clearControlValidity(control);
arrayRemove(controls, control);
};

form.$$clearControlValidity = function(control) {
forEach(form.$pending, clear);
forEach(errors, clear);

function clear(queue, validationToken) {
form.$setValidity(validationToken, true, control);
}
};

form.$$setPending = function(validationToken, control) {
var pending = form.$pending && form.$pending[validationToken];

if (!pending || !includes(pending, control)) {
pendingCount++;
form.$valid = form.$invalid = undefined;
form.$pending = form.$pending || {};
if (!pending) {
pending = form.$pending[validationToken] = [];
}
pending.push(control);
parentForm.$$setPending(validationToken, form);
}
};

/**
* @ngdoc method
Expand All @@ -186,72 +161,33 @@ function FormController(element, attrs, $scope, $animate) {
*
* This method will also propagate to parent forms.
*/
form.$setValidity = function(validationToken, isValid, control) {
var queue = errors[validationToken];
var pendingChange, pending = form.$pending && form.$pending[validationToken];

if (pending) {
pendingChange = pending.indexOf(control) >= 0;
if (pendingChange) {
arrayRemove(pending, control);
pendingCount--;

if (pending.length === 0) {
delete form.$pending[validationToken];
}
}
}

var pendingNoMore = form.$pending && pendingCount === 0;
if (pendingNoMore) {
form.$pending = undefined;
}

if (isValid) {
if (queue || pendingChange) {
if (queue) {
arrayRemove(queue, control);
}
if (!queue || !queue.length) {
if (errors[validationToken]) {
invalidCount--;
}
if (!invalidCount) {
if (!form.$pending) {
toggleValidCss(isValid);
form.$valid = true;
form.$invalid = false;
}
} else if(pendingNoMore) {
toggleValidCss(false);
form.$valid = false;
form.$invalid = true;
}
errors[validationToken] = false;
toggleValidCss(true, validationToken);
parentForm.$setValidity(validationToken, true, form);
addSetValidityMethod({
ctrl: this,
$element: element,
set: function(object, property, control) {
var list = object[property];
if (!list) {
object[property] = [control];
} else {
var index = list.indexOf(control);
if (index === -1) {
list.push(control);
}
}
} else {
if (!form.$pending) {
form.$valid = false;
form.$invalid = true;
},
unset: function(object, property, control) {
var list = object[property];
if (!list) {
return;
}

if (!invalidCount) {
toggleValidCss(isValid);
arrayRemove(list, control);
if (list.length === 0) {
delete object[property];
}
if (queue) {
if (includes(queue, control)) return;
} else {
errors[validationToken] = queue = [];
invalidCount++;
toggleValidCss(false, validationToken);
parentForm.$setValidity(validationToken, false, form);
}
queue.push(control);
}
};
},
parentForm: parentForm,
$animate: $animate
});

/**
* @ngdoc method
Expand Down
Loading

0 comments on commit 09c8079

Please sign in to comment.